Les traits en Rust
Un trait définit un ensemble de comportements qu’un type peut implémenter, un peu comme une interface dans d’autres langages. C’est la façon dont Rust dit « ce type sait faire X », et c’est ce qui fait fonctionner les trait bounds (contraintes de trait) que vous avez vues à la page Les génériques.
Définir un trait
Section intitulée « Définir un trait »trait Speak { fn speak(&self) -> String;}Cela veut dire : tout type qui implémente Speak doit fournir une méthode speak qui renvoie une String. Tout seul, un trait n’est qu’une promesse : il ne fait rien tant qu’aucun type ne l’implémente.
Implémenter un trait
Section intitulée « Implémenter un trait »trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Wouf !") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Miaou !") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}Sortie :
Wouf !Miaou !Dog et Cat sont deux types sans aucun lien, mais tous deux implémentent Speak : on peut donc appeler .speak() sur l’un comme sur l’autre. C’est le même principe que les blocs impl de la page Les structs : impl Speak for Dog dit simplement « voici comment Dog remplit le contrat du trait Speak ».
Les traits comme paramètres de fonction
Section intitulée « Les traits comme paramètres de fonction »Voici le vrai intérêt : vous pouvez écrire une fonction qui accepte n’importe quel type implémentant un trait, sans vous soucier du type précis :
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("Wouf !") }}
fn announce(animal: &impl Speak) { println!("L'animal dit : {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak veut dire « n’importe quel type qui implémente Speak, peu importe lequel ». Ajoutez un type Bird qui implémente lui aussi Speak, et announce fonctionnera avec, sans rien changer.
Des traits que vous utilisez déjà
Section intitulée « Des traits que vous utilisez déjà »Vous vous souvenez de #[derive(Debug)] sur la page Les structs ? Debug est lui-même un trait : derive génère simplement l’implémentation à votre place, au lieu que vous l’écriviez à la main. D’autres traits courants que vous croiserez : Clone (pour .clone()), PartialEq (pour ==) et PartialOrd (pour < et >, que vous avez vu servir de trait bound sur la page des génériques).
Les implémentations par défaut
Section intitulée « Les implémentations par défaut »Un trait peut fournir un corps de méthode par défaut, que les types qui l’implémentent peuvent garder tel quel ou remplacer :
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Bonjour, {} !", self.name()) }}Un type qui implémente Greet n’a qu’à définir name() : greet() est offerte, sauf s’il choisit de la redéfinir aussi.
Les traits ne s’arrêtent pas là. La page Les traits courants passe en revue ceux de la bibliothèque standard que vous utiliserez tous les jours, et Traits avancés présente les types associés, les supertraits et d’autres outils que vous verrez dans les gros projets.
Exercices
Section intitulée « Exercices »1. Des formes avec une aire
Section intitulée « 1. Des formes avec une aire »Définissez un trait Area avec une seule méthode, fn area(&self) -> f64. Implémentez-le pour un Rectangle (avec width et height) et un Square (avec side). Affichez l’aire d’un rectangle de 3 sur 4 et d’un carré de côté 5. Indice : chaque bloc impl Area for ... n’a besoin que de cette méthode, et son corps tient en une multiplication.
Voir la solution
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("Rectangle : {}", rect.area()); println!("Carré : {}", square.area());}Sortie :
Rectangle : 12Carré : 252. Une fonction pour plusieurs types
Section intitulée « 2. Une fonction pour plusieurs types »Définissez un trait Describe avec fn describe(&self) -> String. Implémentez-le pour un Book (avec un title) et un Movie (avec un title et des minutes). Écrivez ensuite une seule fonction fn show(item: &impl Describe) qui affiche la description, et appelez-la avec les deux. Indice : utilisez format! pour construire chaque description.
Voir la solution
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("Livre : {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("Film : {} ({} min)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("Le Hobbit") }; let movie = Movie { title: String::from("Là-haut"), minutes: 96 };
show(&book); show(&movie);}Sortie :
Livre : Le HobbitFilm : Là-haut (96 min)3. Garder ou remplacer une méthode par défaut
Section intitulée « 3. Garder ou remplacer une méthode par défaut »Reprenez le trait Greet de cette page, avec sa méthode par défaut greet(). Implémentez-le pour un Person, qui garde la version par défaut, et pour un Pirate, qui redéfinit greet() pour dire plutôt Ohé, ... !. Indice : les deux types doivent définir name(). Seul Pirate écrit en plus son propre greet().
Voir la solution
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Bonjour, {} !", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("Ohé, {} !", self.name()) }}
fn main() { let ana = Person { name: String::from("Ana") }; let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet()); println!("{}", jack.greet());}Sortie :
Bonjour, Ana !Ohé, Jack !Récapitulatif
Section intitulée « Récapitulatif »- Un trait définit un comportement qu’un type peut implémenter, à la manière d’une interface.
impl NomDuTrait for Typefournit l’implémentation.&impl NomDuTraitcomme type de paramètre accepte n’importe quel type qui implémente ce trait.- Un trait peut fournir des corps de méthode par défaut, que les implémentations gardent ou remplacent.
Debug,CloneetPartialEqsont des traits courants de la bibliothèque standard :#[derive(...)]les implémente automatiquement.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que définit un trait ?
2. Que signifie animal: &impl Speak comme type de paramètre ?
3. Que fait réellement #[derive(Debug)] ?
4. Qu'est-ce qu'une implémentation par défaut dans un trait ?
5. D'après cette page, lequel de ces noms est un trait, et non une struct ou une enum ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Traits: Defining Shared Behavior — The Rust Book (en anglais) : le chapitre officiel sur la définition des traits, les méthodes par défaut, les paramètres
impl Traitet les trait bounds. - Traits — Rust by Example (en anglais) : un exemple exécutable avec un trait, une méthode par défaut et une implémentation.
- Derive — Rust by Example (en anglais) : les traits standard que
#[derive(...)]peut implémenter pour vous, avec des exemples. - Advanced Traits — The Rust Book (en anglais) : l’étape suivante, avec les types associés, les supertraits et le motif newtype.
- Traits — The Rust Reference (en anglais) : les règles précises de définition des traits, pour quand vous voulez tous les détails.