Traits#
Les traits sont le mécanisme fondamental d’abstraction en Rust. Ils définissent un ensemble de comportements qu’un type peut implémenter, jouant un role analogue aux interfaces d’autres langages tout en offrant des possibilités supplémentaires : méthodes par défaut, dispatch statique et dynamique, bornes composées. Ils constituent le socle du polymorphisme en Rust, en remplacement de l’héritage de classes (cf. chapitre 8).
Définition d’un trait#
Définition 70 (Trait)
Un trait définit un ensemble de méthodes qu’un type s’engage a fournir. Il se déclare avec le mot-clé trait, suivi d’un nom et d’un bloc contenant les signatures des méthodes requises (sans corps) et, éventuellement, des méthodes par défaut (avec corps).
trait Aire {
fn aire(&self) -> f64; // méthode requise
fn description(&self) -> String { // méthode par défaut
format!("Aire = {:.2}", self.aire())
}
}
Les méthodes requises n’ont pas de corps : tout type implémentant le trait doit les définir. Les méthodes par défaut fournissent une implémentation utilisable telle quelle ou remplaçable.
Implémentation d’un trait#
Définition 71 (Implémentation de trait)
La syntaxe impl Trait for Type fournit les définitions concrètes des méthodes du trait pour un type donné. Chaque méthode requise doit être définie ; les méthodes par défaut peuvent être omises ou redéfinies.
Exemple 73 (Implémentation pour plusieurs types)
Voir le code ci-dessous.
trait Aire {
fn aire(&self) -> f64;
fn description(&self) -> String { format!("Aire = {:.2}", self.aire()) }
}
struct Cercle { rayon: f64 }
struct Rectangle { largeur: f64, hauteur: f64 }
impl Aire for Cercle {
fn aire(&self) -> f64 { std::f64::consts::PI * self.rayon * self.rayon }
}
impl Aire for Rectangle {
fn aire(&self) -> f64 { self.largeur * self.hauteur }
fn description(&self) -> String { // redéfinition de la méthode par défaut
format!("Rectangle {}x{}, aire = {:.2}", self.largeur, self.hauteur, self.aire())
}
}
let c = Cercle { rayon: 3.0 };
let r = Rectangle { largeur: 4.0, hauteur: 5.0 };
println!("{}", c.description()); // méthode par défaut
println!("{}", r.description()); // redéfinition
Aire = 28.27
Rectangle 4x5, aire = 20.00
Remarque 54
Contrairement aux langages a héritage simple, un type Rust peut implémenter librement plusieurs traits indépendants. Cela favorise la composition de comportements plutôt que la hiérarchie de classes.
Traits de la bibliothèque standard#
La bibliothèque standard définit de nombreux traits que l’on retrouve dans tout programme Rust. L’attribut #[derive(...)] permet d’en générer automatiquement l’implémentation (cf. chapitre 8).
Debug et Display#
Définition 72 (Debug et Display)
Debug (std::fmt::Debug) produit une représentation de débogage ({:?}), dérivable automatiquement. Display (std::fmt::Display) produit une représentation lisible par l’utilisateur ({}), toujours implémenté manuellement.
use std::fmt;
struct Temperature(f64);
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1} °C", self.0) }
}
impl fmt::Debug for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Temperature({:.2})", self.0) }
}
let t = Temperature(36.6);
println!("Display : {} | Debug : {:?}", t, t);
Display : 36.6 °C | Debug : Temperature(36.60)
Clone et Copy#
Clone fournit la méthode clone() pour une copie explicite. Copy est un marker trait (sans méthode) qui autorise la copie implicite lors des affectations. Un type Copy doit aussi être Clone.
#[derive(Debug, Clone, Copy)]
struct Point { x: f64, y: f64 }
let a = Point { x: 1.0, y: 2.0 };
let b = a; // copie implicite grâce a Copy
let c = a; // a est toujours utilisable
println!("a={:?}, b={:?}, c={:?}", a, b, c);
a=Point { x: 1.0, y: 2.0 }, b=Point { x: 1.0, y: 2.0 }, c=Point { x: 1.0, y: 2.0 }
Remarque 55
Seuls les types dont toutes les composantes sont Copy peuvent dériver Copy. Un type contenant un String ou un Vec ne le peut pas, car ces types gèrent de la mémoire sur le tas (cf. chapitre 5).
PartialEq, Eq, PartialOrd, Ord#
PartialEq et PartialOrd autorisent des relations partielles (par exemple, f64 implémente PartialEq mais pas Eq, car NaN != NaN). Eq et Ord requièrent respectivement une relation d’équivalence et un ordre total.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Score { points: u32, nom: String }
let a = Score { points: 100, nom: String::from("Alice") };
let b = Score { points: 85, nom: String::from("Bob") };
println!("a == a : {} | a > b : {}", a == a, a > b);
a == a : true | a > b : true
Default et From/Into#
#[derive(Debug, Default)]
struct Config { largeur: u32, hauteur: u32, plein_ecran: bool }
println!("{:?}", Config::default());
Config { largeur: 0, hauteur: 0, plein_ecran: false }
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) }
}
let f = Fahrenheit::from(Celsius(100.0));
println!("{:.1} °F", f.0);
// Into est automatiquement disponible quand From est implémenté
let f2: Fahrenheit = Celsius(37.0).into();
println!("{:.1} °F", f2.0);
212.0 °F
98.6 °F
Proposition 13 (Symétrie From / Into)
Lorsque From<A> est implémenté pour B, le compilateur fournit automatiquement Into<B> pour A. Il est donc idiomatique de n’implémenter que From.
Règle de l’orphelin#
Proposition 14 (Règle de l’orphelin (orphan rule))
Pour implémenter un trait T pour un type S, au moins l’un des deux — le trait ou le type — doit être défini dans le crate courant. Il est interdit d’implémenter un trait étranger pour un type étranger.
Cette règle garantit la cohérence : sans elle, deux crates pourraient fournir des implémentations contradictoires du même trait pour le même type.
// Impossible : Display (étranger) pour Vec<i32> (étranger)
impl std::fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "vecteur") }
}
impl std::fmt::Display for Vec<i32> {
^^^^^^^^ `Vec` is not defined in the current crate
impl std::fmt::Display for Vec<i32> {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: only traits defined in the current crate can be implemented for types defined outside of the crate
only traits defined in the current crate can be implemented for types defined outside of the crate
Remarque 56
Le newtype pattern (chapitre 8) permet de contourner cette restriction : on encapsule le type étranger dans un type local, puis on implémente le trait sur ce dernier.
Traits comme paramètres#
Définition 73 (impl Trait en paramètre)
La notation fn f(x: impl Trait) est un sucre syntaxique pour fn f<T: Trait>(x: T). La fonction accepte tout type implémentant le trait donné. Chaque appel avec un type différent produit une spécialisation distincte par monomorphisation (dispatch statique).
Exemple 74 (Fonction avec impl Trait)
Voir le code ci-dessous.
use std::fmt;
fn afficher_deux_fois(val: impl fmt::Display) {
println!("{}", val);
println!("{}", val);
}
afficher_deux_fois(42);
afficher_deux_fois("bonjour");
42
42
bonjour
bonjour
On peut combiner plusieurs bornes avec +.
use std::fmt;
fn inspecter(val: impl fmt::Display + fmt::Debug) {
println!("Display : {} | Debug : {:?}", val, val);
}
inspecter(42);
inspecter("texte");
Display : 42 | Debug : 42
Display : texte | Debug : "texte"
Remarque 57
La syntaxe impl Trait et les génériques explicites sont équivalents pour les cas simples. Pour des signatures complexes (plusieurs paramètres devant partager le même type, clauses where), la forme générique explicite est préférable. Les génériques seront approfondis au chapitre 14.
Objets-traits et dispatch dynamique#
Définition 74 (Objet-trait)
Un objet-trait (trait object) est une valeur de type dyn Trait qui effectue un dispatch dynamique : la méthode concrète est résolue a l’exécution via une table de fonctions virtuelles (vtable). On manipule les objets-traits derrière un pointeur : &dyn Trait, Box<dyn Trait>, etc.
Le dispatch dynamique est nécessaire lorsque le type concret n’est pas connu a la compilation, notamment pour stocker des valeurs de types différents dans une même collection.
Exemple 75 (Collection hétérogène avec Box<dyn Trait>)
Voir le code ci-dessous.
trait Animal {
fn cri(&self) -> &str;
fn nom(&self) -> &str;
}
struct Chat { nom: String }
struct Chien { nom: String }
impl Animal for Chat {
fn cri(&self) -> &str { "miaou" }
fn nom(&self) -> &str { &self.nom }
}
impl Animal for Chien {
fn cri(&self) -> &str { "ouaf" }
fn nom(&self) -> &str { &self.nom }
}
let animaux: Vec<Box<dyn Animal>> = vec![
Box::new(Chat { nom: String::from("Félix") }),
Box::new(Chien { nom: String::from("Rex") }),
];
for a in &animaux {
println!("{} fait {}", a.nom(), a.cri());
}
Félix fait miaou
Rex fait ouaf
Dispatch statique vs dynamique#
Aspect |
Statique ( |
Dynamique ( |
|---|---|---|
Résolution |
Compilation (monomorphisation) |
Exécution (vtable) |
Performance |
Pas de surcout, inlining possible |
Indirection via pointeur |
Taille du binaire |
Une copie par type concret |
Une seule copie du code |
Collections hétérogènes |
Non |
Oui |
Object safety#
Proposition 15 (Object safety)
Un trait est object-safe — c’est-a-dire utilisable comme dyn Trait — si et seulement si aucune de ses méthodes ne retourne Self et aucune ne possède de paramètres de type générique. Un trait non object-safe ne peut pas être utilisé pour créer un objet-trait.
// Clone n'est pas object-safe car clone() retourne Self
let _v: Vec<Box<dyn Clone>> = vec![];
[E0038] Error: the trait `Clone` is not dyn compatible
╭─[command_13:1:1]
│
2 │ let _v: Vec<Box<dyn Clone>> = vec![];
│ ──┬──
│ ╰──── `Clone` is not dyn compatible
───╯
Remarque 58
La restriction sur Self existe parce que, lors du dispatch dynamique, le compilateur ne connait pas la taille concrète du type. Une méthode retournant Self nécessiterait cette information a la compilation, ce qui est incompatible avec la résolution a l’exécution.
Supertraits#
Définition 75 (Supertrait)
Un supertrait est un trait requis comme prérequis par un autre trait. La syntaxe trait A: B signifie que tout type implémentant A doit aussi implémenter B. On parle aussi de borne de trait sur un trait (trait bound on a trait).
Exemple 76 (Supertrait et bornes composées)
Voir le code ci-dessous.
use std::fmt;
trait Affichable: fmt::Display + fmt::Debug {
fn afficher(&self) {
println!("Display : {} | Debug : {:?}", self, self);
}
}
#[derive(Debug)]
struct Identifiant(u64);
impl fmt::Display for Identifiant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ID-{:06}", self.0) }
}
impl Affichable for Identifiant {}
Identifiant(42).afficher();
Display : ID-000042 | Debug : Identifiant(42)
Remarque 59
Les supertraits ne sont pas de l’héritage au sens de la programmation orientée objet : il n’y a pas de relation « est-un » entre les types, ni de données héritées. Il s’agit uniquement d’une contrainte sur les traits qu’un type doit implémenter.
Send et Sync#
Définition 76 (Send et Sync)
Send et Sync sont des marker traits (traits sans méthode) définis dans std::marker qui régissent la sécurité de la concurrence :
un type est
Sends’il peut être transféré d’un thread a un autre ;un type est
Syncsi une référence partagée&Tpeut être envoyée a un autre thread en toute sécurité.
Proposition 16 (Implémentation automatique)
Le compilateur implémente automatiquement Send et Sync pour tout type composé exclusivement de champs Send (respectivement Sync). Les exceptions notables sont Rc<T> (ni Send ni Sync) et Cell<T> / RefCell<T> (non Sync).
fn est_send<T: Send>() {}
fn est_sync<T: Sync>() {}
est_send::<i32>();
est_sync::<String>();
est_send::<Vec<f64>>();
println!("i32, String et Vec<f64> sont Send + Sync");
i32, String et Vec<f64> sont Send + Sync
use std::rc::Rc;
fn est_send<T: Send>() {}
est_send::<Rc<i32>>(); // Rc n'est pas Send
[E0277] Error: `Rc<i32>` cannot be sent between threads safely
╭─[command_16:1:1]
│
2 │ fn est_send<T: Send>() {}
│ ──┬─
│ ╰─── required by this bound in `est_send`
3 │ est_send::<Rc<i32>>(); // Rc n'est pas Send
│ ───┬───
│ ╰───── `Rc<i32>` cannot be sent between threads safely
───╯
Remarque 60
Send et Sync sont la clé de la garantie de Rust contre les data races a la compilation. Un programme qui compile ne peut pas contenir de data race sur des données partagées entre threads. Ces traits seront étudiés en détail au chapitre 19 consacré a la concurrence.
Résumé#
Les traits structurent l’ensemble de l’écosystème Rust :
ils définissent des contrats que les types s’engagent a respecter ;
ils permettent le polymorphisme sans héritage, par dispatch statique (génériques,
impl Trait) ou dynamique (dyn Trait) ;les traits de la bibliothèque standard (
Display,Clone,From, etc.) fournissent un vocabulaire partagé entre tous les crates ;la règle de l’orphelin assure la cohérence globale du système de traits ;
SendetSyncexploitent le système de traits pour garantir la sécurité de la concurrence.
Le chapitre 14 approfondira les génériques et les bornes de traits dans des contextes plus avancés. Le chapitre 19 montrera comment Send et Sync s’intègrent dans le modèle de concurrence de Rust.