Aller au contenu

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.

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.

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 !
▶ Essayez par vous-même

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 ».

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);
}
▶ Essayez par vous-même

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.

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).

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.

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 : 12
Carré : 25
▶ Essayez par vous-même

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 Hobbit
Film : Là-haut (96 min)
▶ Essayez par vous-même

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 !
▶ Essayez par vous-même
  • Un trait définit un comportement qu’un type peut implémenter, à la manière d’une interface.
  • impl NomDuTrait for Type fournit l’implémentation.
  • &impl NomDuTrait comme 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, Clone et PartialEq sont des traits courants de la bibliothèque standard : #[derive(...)] les implémente automatiquement.

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