Aller au contenu

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

La macro dbg!, que vous verrez peut-être dans d’autres programmes, utilise elle aussi Debug.

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 Herbert
Ce texte fait 21 caractères
▶ Essayez par vous-même

write! 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.

Les deux créent un double d’une valeur, mais de façons différentes.

  • Clone vous donne une méthode .clone(). Vous l’appelez volontairement, et elle peut faire un vrai travail, comme recopier tout le texte d’une String.
  • Copy cré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" }
▶ Essayez par vous-même

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 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 : true
a != c : true
▶ Essayez par vous-même

Et 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 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 : true
1.2.0
1.10.3
2.0.0
▶ Essayez par vous-même

Comme 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 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: "" }
▶ Essayez par vous-même

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 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 °F
La température du corps est de 98.6 °F
▶ Essayez par vous-même

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

La page Les erreurs personnalisées développe cette idée plus en détail.

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

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.

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.

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 expansion
2 | 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 expansion
2 | 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.

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 ? true
Task { priority: 1, title: "Payer le loyer" }
Task { priority: 2, title: "Répondre à Sam" }
Task { priority: 3, title: "Arroser les plantes" }
▶ Essayez par vous-même

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 €
▶ Essayez par vous-même
  • Debug ({:?}) est pour les développeurs et se dérive presque toujours. Display ({}) est pour les utilisateurs et s’écrit à la main.
  • Clone copie sur demande avec .clone(). Copy copie automatiquement, mais seulement pour les types simples.
  • PartialEq/Eq vous donnent ==. PartialOrd/Ord vous donnent < et le tri, en comparant les champs de haut en bas.
  • Default construit une valeur de départ ; ..Default::default() remplit les champs que vous n’avez pas fixés.
  • Implémentez From et vous obtenez Into gratuitement. L’opérateur ? utilise From pour 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

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