iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 6·35 min

Traits et génériques

Un trait décrit un comportement : « ce type sait s'afficher », « ce type sait se comparer », « ce type a une aire ». Un générique est du code écrit une fois pour plusieurs types. Ensemble, ils remplacent ce que d'autres langages font avec les interfaces et l'héritage, sans coût à l'exécution dans le cas courant.

Si vous venez de TypeScript, un trait ressemble à une interface, avec une différence de taille : on peut implémenter un trait pour un type qu'on n'a pas écrit, y compris i32 ou String. L'article sur TypeScript pour les développeurs JavaScript permet de comparer les deux approches.

Définir et implémenter un trait

trait Resume {
    fn auteur(&self) -> String;
 
    // méthode avec une implémentation par défaut
    fn resumer(&self) -> String {
        format!("(lire la suite de {}...)", self.auteur())
    }
}
 
struct Article {
    titre: String,
    auteur: String,
}
 
struct Tweet {
    pseudo: String,
    contenu: String,
}
 
impl Resume for Article {
    fn auteur(&self) -> String {
        self.auteur.clone()
    }
    fn resumer(&self) -> String {
        format!("{}, par {}", self.titre, self.auteur)
    }
}
 
impl Resume for Tweet {
    fn auteur(&self) -> String {
        format!("@{}", self.pseudo)
    }
    // resumer : on garde la version par défaut
}
 
fn main() {
    let a = Article { titre: "Rust en production".into(), auteur: "Ada".into() };
    let t = Tweet { pseudo: "linus".into(), contenu: "Bonjour".into() };
    println!("{}", a.resumer());
    println!("{} — {}", t.resumer(), t.contenu);
}

.into() convertit un &str en String : on reviendra sur ce mécanisme avec le trait From.

Fonctions génériques

Une fonction peut accepter n'importe quel type qui implémente un trait :

fn notifier(element: &impl Resume) {
    println!("Nouveau : {}", element.resumer());
}

impl Resume est un raccourci. La forme complète, avec un paramètre de type, permet plus de choses :

fn notifier<T: Resume>(element: &T) {
    println!("Nouveau : {}", element.resumer());
}
 
// deux paramètres du même type T
fn comparer<T: Resume>(a: &T, b: &T) { /* ... */ }
 
// plusieurs contraintes : la clause where reste lisible
fn journaliser<T>(element: &T)
where
    T: Resume + std::fmt::Debug,
{
    println!("{:?} -> {}", element, element.resumer());
}

T: Resume se lit « n'importe quel type T qui implémente Resume ». C'est une contrainte (un « trait bound »). Sans elle, le compilateur refuserait l'appel à resumer, car il ne saurait pas que T possède cette méthode.

Un exemple classique, le plus grand élément d'une liste :

fn plus_grand<T: PartialOrd + Copy>(liste: &[T]) -> Option<T> {
    let mut max = *liste.first()?;
    for &x in liste {
        if x > max {
            max = x;
        }
    }
    Some(max)
}
 
fn main() {
    println!("{:?}", plus_grand(&[3, 17, 8]));        // Some(17)
    println!("{:?}", plus_grand(&[2.5, -1.0]));       // Some(2.5)
    println!("{:?}", plus_grand(&['r', 'u', 's']));   // Some('u')
    println!("{:?}", plus_grand::<i32>(&[]));         // None
}

PartialOrd autorise >, Copy autorise la copie de x dans max. Le compilateur génère une version de la fonction pour chaque type utilisé (i32, f64, char). Ce procédé, la monomorphisation, rend les génériques aussi rapides que du code écrit à la main pour chaque type.

Structs génériques

#[derive(Debug)]
struct Paire<T> {
    a: T,
    b: T,
}
 
impl<T: PartialOrd + std::fmt::Display> Paire<T> {
    fn afficher_max(&self) {
        let max = if self.a > self.b { &self.a } else { &self.b };
        println!("le plus grand est {max}");
    }
}
 
fn main() {
    Paire { a: 3, b: 9 }.afficher_max();
    Paire { a: "pomme", b: "kiwi" }.afficher_max();
}

Option<T>, Result<T, E>, Vec<T> et HashMap<K, V> sont construits exactement ainsi.

Les traits de la bibliothèque standard

Une bonne partie du travail consiste à implémenter, ou à dériver, des traits standards :

TraitPermetDérivable
Debug{:?}Oui
Clone.clone()Oui
CopyCopie implicite (types simples)Oui
PartialEq, Eq== et !=Oui
PartialOrd, Ord<, >, triOui
HashClé de HashMapOui
DefaultType::default()Oui
Display{}Non, à écrire
From / IntoConversionsNon, à écrire
IteratorBoucles for, adaptateursNon, à écrire

#[derive(...)] génère l'implémentation évidente, champ par champ :

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
    majeure: u32,
    mineure: u32,
    correctif: u32,
}
 
fn main() {
    let mut versions = vec![
        Version { majeure: 1, mineure: 10, correctif: 0 },
        Version { majeure: 1, mineure: 2, correctif: 3 },
        Version::default(),
    ];
    versions.sort(); // ordre des champs : majeure, puis mineure, puis correctif
    println!("{versions:?}");
}

Mon réflexe sur toute nouvelle struct de données : #[derive(Debug, Clone, PartialEq)], et j'ajoute le reste quand le compilateur le réclame.

Display

use std::fmt;
 
struct Version {
    majeure: u32,
    mineure: u32,
    correctif: u32,
}
 
impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "v{}.{}.{}", self.majeure, self.mineure, self.correctif)
    }
}
 
fn main() {
    let v = Version { majeure: 2, mineure: 0, correctif: 1 };
    println!("{v}");               // v2.0.1
    let texte = v.to_string();     // offert par Display
    println!("{}", texte.len());
}

Implémenter Display donne .to_string() gratuitement : la bibliothèque standard l'implémente pour tout type qui a Display.

From et Into

struct Celsius(f64);
struct Fahrenheit(f64);
 
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}
 
fn main() {
    let f = Fahrenheit::from(Celsius(100.0));
    let g: Fahrenheit = Celsius(0.0).into(); // Into est offert par From
    println!("{} {}", f.0, g.0);              // 212 32
}

From sert aussi à l'opérateur ? : si votre type d'erreur implémente From<io::Error>, un ? sur une opération de fichier convertit l'erreur automatiquement.

Le polymorphisme dynamique : dyn Trait

Les génériques fixent le type à la compilation. Parfois, on veut une liste qui mélange des types différents, tous dotés du même comportement. On utilise alors un objet trait :

trait Forme {
    fn aire(&self) -> f64;
    fn nom(&self) -> &str;
}
 
struct Cercle(f64);
struct Carre(f64);
 
impl Forme for Cercle {
    fn aire(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
    fn nom(&self) -> &str { "cercle" }
}
 
impl Forme for Carre {
    fn aire(&self) -> f64 { self.0 * self.0 }
    fn nom(&self) -> &str { "carré" }
}
 
fn main() {
    let formes: Vec<Box<dyn Forme>> = vec![Box::new(Cercle(1.0)), Box::new(Carre(2.0))];
    let total: f64 = formes.iter().map(|f| f.aire()).sum();
    for f in &formes {
        println!("{} : {:.2}", f.nom(), f.aire());
    }
    println!("total : {total:.2}");
}

Box<dyn Forme> est un pointeur vers « une valeur d'un type inconnu qui implémente Forme ». L'appel de méthode passe par une table de fonctions à l'exécution, un léger coût.

Générique <T: Trait>Objet trait dyn Trait
Type connuÀ la compilationÀ l'exécution
Liste de types mélangésNonOui
CoûtAucun, mais binaire plus grosUn appel indirect
Usage courantFonctions utilitaires, bibliothèquesPlugins, listes hétérogènes

Par défaut, utilisez les génériques. Passez à dyn quand vous avez besoin de mélanger des types. Et si la liste des types est fermée et connue, un enum, comme au chapitre 4, est souvent encore plus simple.

Exercice

Écrivez un trait Taxable avec une méthode prix_ht(&self) -> f64 et une méthode par défaut prix_ttc(&self) -> f64 qui applique 20 % de TVA. Implémentez-le pour Livre (TVA réduite à 5,5 % : redéfinissez prix_ttc) et pour Ordinateur. Écrivez une fonction générique qui calcule le total TTC d'une tranche de Taxable.

trait Taxable {
    fn prix_ht(&self) -> f64;
    fn prix_ttc(&self) -> f64 {
        self.prix_ht() * 1.20
    }
}
 
struct Livre { prix: f64 }
struct Ordinateur { prix: f64 }
 
impl Taxable for Livre {
    fn prix_ht(&self) -> f64 { self.prix }
    fn prix_ttc(&self) -> f64 { self.prix * 1.055 }
}
 
impl Taxable for Ordinateur {
    fn prix_ht(&self) -> f64 { self.prix }
}
 
fn total_ttc<T: Taxable>(articles: &[T]) -> f64 {
    articles.iter().map(|a| a.prix_ttc()).sum()
}
 
fn main() {
    let livres = [Livre { prix: 20.0 }, Livre { prix: 12.0 }];
    let pc = [Ordinateur { prix: 1000.0 }];
    println!("{:.2} {:.2}", total_ttc(&livres), total_ttc(&pc)); // 33.76 1200.00
}

Pour un panier qui mélange livres et ordinateurs, il faudrait &[Box<dyn Taxable>]. Essayez de l'écrire.


Les traits décrivent ce qu'un type sait faire, les génériques écrivent du code pour tous les types qui savent le faire, sans coût à l'exécution. Il reste une pièce du système de types à comprendre avant d'être à l'aise : les durées de vie, au chapitre suivant, avec les pointeurs intelligents.

Précédent
Collections et gestion d'erreurs
Suivant
Durées de vie et pointeurs intelligents

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main