Les traits courants en Rust
Sur la page Les traits, vous avez écrit vos propres traits. Mais la bibliothèque standard fournit déjà une série de traits que presque tous les programmes Rust utilisent. Ils répondent à des questions simples sur un type : Est-ce que je peux l’afficher ? Le copier ? Le comparer ? En créer un vide ?
Voyez-les comme des badges sur un uniforme. Chaque badge dit « ce type sait faire X ». Beaucoup de ces badges s’obtiennent gratuitement avec #[derive(...)]. D’autres s’écrivent à la main. Cette page passe en revue ceux que vous croiserez le plus souvent, et se termine par un tableau que vous pourrez venir consulter.
Debug : afficher pour les développeurs
Section intitulée « Debug : afficher pour les développeurs »Debug permet d’afficher une valeur avec {:?}. Il est fait pour vous, le développeur : pour vérifier des valeurs pendant que vous écrivez et corrigez votre code. On ne l’écrit presque jamais à la main ; on le dérive, tout simplement. Utilisez {:#?} pour une version « jolie », avec un champ par ligne, plus facile à lire pour les grosses structs :
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}Sortie :
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}La macro dbg!, que vous verrez peut-être dans d’autres programmes, utilise elle aussi Debug.
Display : afficher pour les utilisateurs
Section intitulée « Display : afficher pour les utilisateurs »Display est ce qu’utilise {}. Il sert au texte destiné aux utilisateurs : le message qu’une personne lit à l’écran. Rust ne peut pas deviner à quoi il doit ressembler, donc il n’y a pas de derive. Vous l’écrivez vous-même en implémentant fmt::Display :
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} de {}", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("Ce texte fait {} caractères", text.len());}Sortie :
Dune de Frank HerbertCe texte fait 21 caractèreswrite! fonctionne comme println!, mais écrit dans le formateur f au lieu de l’écran. Petit bonus : dès qu’un type implémente Display, vous obtenez .to_string() gratuitement. (Ici, tout le texte mesuré est en ASCII, donc .len(), qui compte des octets, donne bien le nombre de caractères.) Vous retrouverez Display sur la page Les erreurs personnalisées, où il donne aux erreurs un message lisible.
Clone et Copy
Section intitulée « Clone et Copy »Les deux créent un double d’une valeur, mais de façons différentes.
Clonevous donne une méthode.clone(). Vous l’appelez volontairement, et elle peut faire un vrai travail, comme recopier tout le texte d’uneString.Copycrée un double automatiquement chaque fois que vous affectez ou passez la valeur. Ce n’est permis que pour les petits types simples qu’on peut copier bit à bit : les nombres,bool,char, et les structs composées uniquement de ceux-là.
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy : p1 reste utilisable println!("{:?} et {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone : on demande une copie println!("{:?} et {:?}", u1, u2);}Sortie :
Point { x: 1, y: 2 } et Point { x: 1, y: 2 }User { name: "Ana" } et User { name: "Ana" }Sans Copy, let p2 = p1; déplacerait p1 (un move), et vous ne pourriez plus l’utiliser ensuite (voir L’ownership). Notez que Copy exige toujours Clone aussi : c’est pour ça qu’on dérive les deux ensemble.
PartialEq et Eq : comparer avec ==
Section intitulée « PartialEq et Eq : comparer avec == »PartialEq permet d’utiliser == et !=. Quand vous le dérivez, deux valeurs sont égales si tous leurs champs sont égaux :
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b : {}", a == b); println!("a != c : {}", a != c);}Sortie :
a == b : truea != c : trueEt Eq, alors ? Il n’ajoute aucune méthode. C’est une promesse : « toute valeur est toujours égale à elle-même ». Ça paraît évident, mais les nombres à virgule flottante ne la respectent pas : en Rust, f64::NAN == f64::NAN vaut false. Donc f64 a PartialEq mais pas Eq. Il vous faut Eq (plus Hash) pour utiliser votre type comme clé d’une HashMap ou dans un HashSet. Si votre struct n’a aucun champ flottant, dérivez les deux.
PartialOrd et Ord : comparer avec < et >
Section intitulée « PartialOrd et Ord : comparer avec < et > »PartialOrd permet d’utiliser <, >, <= et >=. Ord ajoute un ordre total, ce dont ont besoin .sort(), .max() et .min(). Quand vous les dérivez, Rust compare les champs de haut en bas : d’abord le premier champ, et seulement s’ils sont égaux, il regarde le suivant.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("old < new : {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}Sortie :
old < new : true1.2.01.10.32.0.0Comme les champs sont comparés dans l’ordre, l’ordre dans lequel vous écrivez les champs compte. Ici, major est le plus important, ce qui est exactement ce qu’il faut pour des numéros de version. Pour les enums, les variantes listées en premier comptent comme « plus petites ». Comme pour Eq, les flottants n’ont que PartialOrd, pas Ord : la page Trier et rechercher montre comment trier des flottants malgré tout.
Default : une valeur de départ raisonnable
Section intitulée « Default : une valeur de départ raisonnable »Default donne à votre type une fonction ::default() qui construit une valeur « de départ ». Quand il est dérivé, chaque champ reçoit sa propre valeur par défaut : 0 pour les nombres, false pour bool, une String vide, un Vec vide. La syntaxe ..Default::default() vous permet de fixer quelques champs et de remplir le reste :
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}Sortie :
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }Si vous voulez d’autres valeurs de départ (par exemple un volume de 50), ne dérivez pas : écrivez vous-même impl Default for Settings, avec une fn default() -> Self qui renvoie les valeurs voulues.
From et Into : convertir d’un type à l’autre
Section intitulée « From et Into : convertir d’un type à l’autre »From décrit comment construire un type à partir d’un autre. Vous l’avez déjà utilisé : String::from("salut"). Quand vous implémentez From, Rust vous donne Into gratuitement, dans l’autre sens :
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 boiling = Fahrenheit::from(Celsius(100.0)); println!("L'eau bout à {} °F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("La température du corps est de {:.1} °F", body.0);}Sortie :
L'eau bout à 212 °FLa température du corps est de 98.6 °FAvec .into(), Rust doit connaître le type d’arrivée : c’est pour ça que nous avons écrit let body: Fahrenheit.
Comment ? utilise From. C’est là que From brille vraiment. Quand l’opérateur ? rencontre une erreur, il appelle From::from pour la transformer dans le type d’erreur que renvoie votre fonction. Donc si vous implémentez From pour votre propre type d’erreur, ? convertit les erreurs à votre place :
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // ici, ParseIntError devient AppError Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}Sortie :
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))La page Les erreurs personnalisées développe cette idée plus en détail.
Surcharger un opérateur avec Add
Section intitulée « Surcharger un opérateur avec Add »En Rust, a + b est en réalité un appel à la méthode add du trait std::ops::Add. Implémentez-le pour votre type, et + fonctionne tout simplement. type Output indique le type que produit l’addition :
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}Sortie :
Point { x: 4, y: 6 }Le même principe marche pour Sub (-), Mul (*), Neg (le - unaire) et AddAssign (+=). Utilisez-le quand le sens est évident : additionner deux points ou deux sommes d’argent. Si un lecteur devait deviner ce que + veut dire pour votre type, une méthode ordinaire avec un nom clair vaut mieux.
Quels traits dériver ?
Section intitulée « Quels traits dériver ? »Voici un petit guide pour une struct typique :
| Trait | Le dériver ? | Ce que vous obtenez |
|---|---|---|
Debug |
Presque toujours | L’affichage avec {:?} et {:#?} |
Clone |
En général | .clone() |
Copy |
Seulement pour les petits types sans champ String, Vec ou Box |
Des copies automatiques au lieu de moves |
PartialEq |
En général | == et != |
Eq |
Quand il n’y a aucun champ flottant | Nécessaire pour les clés de HashMap (avec Hash) |
Hash |
Quand le type sert de clé de HashMap ou va dans un HashSet |
Le hachage |
PartialOrd, Ord |
Quand les valeurs ont un ordre naturel | <, >, .sort(), .max() |
Default |
Quand une valeur « vide » a du sens | ::default() et ..Default::default() |
Display |
Impossible à dériver : écrivez-le à la main | L’affichage avec {} et .to_string() |
Une première ligne très courante est #[derive(Debug, Clone, PartialEq)]. Ajoutez-en d’autres quand vous en avez besoin : le compilateur vous dira quand il en manque un.
Erreurs courantes
Section intitulée « Erreurs courantes »Dériver Copy sur une struct qui contient une String.
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Le compilateur dit que Copy ne peut pas être implémenté pour ce type, parce que le champ name n’implémente pas Copy. Une String possède de la mémoire sur le tas, donc on ne peut pas la copier bit à bit. Retirez Copy et utilisez .clone() quand vous avez besoin d’un double.
Dériver Eq sur une struct qui contient un flottant.
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Ici, le compilateur signale que f64 n’implémente pas Eq. Gardez PartialEq et retirez Eq. Ou bien stockez le prix en centimes entiers dans un u64, ce qui est aussi plus sûr pour de l’argent.
Exercices
Section intitulée « Exercices »1. Trier une liste de tâches
Section intitulée « 1. Trier une liste de tâches »Créez une struct Task avec un priority: u8 et un title: String. Dérivez les traits nécessaires pour pouvoir trier un Vec<Task>, afficher chaque tâche avec {:?} et comparer deux tâches avec ==. Plus le nombre est petit, plus c’est urgent : ces tâches doivent donc venir en premier. Indice : .sort() a besoin de Ord, et Ord a besoin aussi de Eq, PartialEq et PartialOrd. Mettez priority en premier pour qu’il soit comparé en premier.
Voir la solution
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("Arroser les plantes") }, Task { priority: 1, title: String::from("Payer le loyer") }, Task { priority: 2, title: String::from("Répondre à Sam") }, ];
let copy = tasks[1].clone(); println!("Même tâche ? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}Sortie :
Même tâche ? trueTask { priority: 1, title: "Payer le loyer" }Task { priority: 2, title: "Répondre à Sam" }Task { priority: 3, title: "Arroser les plantes" }2. De l’argent qui s’affiche et s’additionne
Section intitulée « 2. De l’argent qui s’affiche et s’additionne »Créez une struct Money qui stocke des centimes entiers, cents: u64. Implémentez From<u64> pour que 350.into() fonctionne, Display pour qu’elle s’affiche comme 3,50 €, et Add pour qu’on puisse additionner deux sommes avec +. Indice : les euros valent cents / 100 et le reste cents % 100. Affichez le reste avec {:02} pour que 5 centimes s’affichent 05. Dérivez Copy pour pouvoir encore afficher les sommes après les avoir additionnées.
Voir la solution
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{},{:02} €", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("Café : {coffee}"); println!("Gâteau : {cake}"); println!("Total : {total}");}Sortie :
Café : 3,50 €Gâteau : 4,05 €Total : 7,55 €Récapitulatif
Section intitulée « Récapitulatif »Debug({:?}) est pour les développeurs et se dérive presque toujours.Display({}) est pour les utilisateurs et s’écrit à la main.Clonecopie sur demande avec.clone().Copycopie automatiquement, mais seulement pour les types simples.PartialEq/Eqvous donnent==.PartialOrd/Ordvous donnent<et le tri, en comparant les champs de haut en bas.Defaultconstruit une valeur de départ ;..Default::default()remplit les champs que vous n’avez pas fixés.- Implémentez
Fromet vous obtenezIntogratuitement. L’opérateur?utiliseFrompour convertir les erreurs. - Implémentez
std::ops::Add(et ses cousins) pour que+marche avec vos propres types.
Envie d’aller plus loin ? Traits avancés explique les types associés comme type Output, les paramètres de type par défaut, et comment implémenter un trait pour un type que vous n’avez pas écrit.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel trait utilise {} dans println! ?
2. Pourquoi une struct avec un champ String ne peut-elle pas dériver Copy ?
3. Quand vous dérivez PartialOrd, comment deux structs sont-elles comparées ?
4. Vous avez implémenté From<ParseIntError> for AppError. Qu'est-ce que cela permet à ? de faire ?
5. Quel trait implémentez-vous pour que a + b fonctionne avec votre propre type ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Derivable Traits — The Rust Book (Appendix C) (en anglais) : tous les traits que vous pouvez
#[derive], ce que fait la version dérivée, et quand vous en aurez besoin. std::fmt— formatting and Display (en anglais) : toutes les options de formatage, comme la largeur, le remplissage et{:#?}, et la place deDisplayetDebug.FromandInto— Rust by Example (en anglais) : de courts exemples exécutables de conversion entre vos propres types.std::ops— operator traits (en anglais) : la liste complète des opérateurs que vous pouvez surcharger, deAddàIndex.std::cmp— comparison traits (en anglais) :PartialEq,Eq,PartialOrdetOrden détail, avec les règles que chacun promet.