Les génériques en Rust
Vous utilisez déjà les génériques sans forcément le savoir : Vec<i32> et Option<T> en sont. Cette page vous apprend à écrire vos propres fonctions et structs génériques.
Le problème que règlent les génériques
Section intitulée « Le problème que règlent les génériques »Imaginez que vous vouliez une fonction qui renvoie le plus grand nombre d’une liste, à la fois pour des i32 et des f64. Sans génériques, il faudrait l’écrire deux fois :
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}La même logique, copiée-collée pour chaque type. Avec les génériques, vous ne l’écrivez qu’une fois.
Une fonction générique
Section intitulée « Une fonction générique »fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}Sortie :
472.8<T: PartialOrd + Copy> se lit ainsi : « pour n’importe quel type T, du moment qu’on peut le comparer (PartialOrd) et le copier (Copy) ». T est un paramètre de type, une sorte de case vide : Rust génère une version de la fonction pour chaque type concret avec lequel vous l’appelez vraiment (i32, f64, etc.).
Une struct générique
Section intitulée « Une struct générique »Les structs peuvent aussi être génériques :
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "bonjour", second: "monde" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}Sortie :
5 10bonjour mondeUne seule définition de Pair<T> fonctionne pour des nombres, du texte ou n’importe quel autre type : numbers est une Pair<i32> et words une Pair<&str>, mais vous n’avez écrit la struct qu’une fois.
Est-ce que ça coûte quelque chose à l’exécution ?
Section intitulée « Est-ce que ça coûte quelque chose à l’exécution ? »Non. Rust génère une version séparée et spécialisée de la fonction ou de la struct pour chaque type concret que vous utilisez réellement (on appelle ça la monomorphisation). Quand votre programme tourne, il ne reste plus aucun code générique : seulement des fonctions ordinaires, entièrement typées, aussi rapides que si vous aviez écrit chacune à la main.
Des méthodes sur une struct générique
Section intitulée « Des méthodes sur une struct générique »Vous pouvez ajouter des méthodes à une struct générique avec un bloc impl, comme sur la page Les structs. La seule nouveauté, c’est que le impl a lui aussi besoin de son propre <T> :
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("gauche", "droite").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}Sortie :
droite gauche8Il y a ici deux sortes de blocs impl :
impl<T> Pair<T>veut dire « ces méthodes existent pour unePairde n’importe quel type ». Lisez le premier<T>comme « pour tout typeT». Sans lui, Rust croirait queTest le nom d’un vrai type et se plaindrait de ne pas le trouver.impl Pair<f64>n’a pas de<T>. Ses méthodes existent uniquement pourPair<f64>. C’est pour ça queaveragepeut utiliser+et/ 2.0. Essayez d’appelerwords.average(): le compilateur vous dira que cette méthode n’existe pas pourPair<&str>.
Une struct peut aussi avoir plusieurs paramètres de type, comme struct Pair<T, U> { first: T, second: U }, quand les deux champs peuvent avoir des types différents. Et quand les trait bounds deviennent longues, vous pouvez les déplacer dans une clause where pour garder une signature lisible. C’est expliqué à la page Traits avancés.
Exercices
Section intitulée « Exercices »1. Un plus petit élément générique
Section intitulée « 1. Un plus petit élément générique »Écrivez fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, qui renvoie le plus petit élément. Testez-la avec [8, 3, 5] et avec les caractères ['r', 'u', 's', 't']. Indice : c’est la fonction largest de cette page avec une comparaison inversée.
Voir la solution
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}Sortie :
3r2. La plus grande moitié d’une Pair
Section intitulée « 2. La plus grande moitié d’une Pair »Ajoutez à Pair<T> une méthode larger(&self) -> &T qui renvoie une référence vers le plus grand des deux champs. Testez-la avec une Pair de nombres et une Pair de mots. Indice : comparer demande PartialOrd, donc mettez la contrainte directement sur le impl : impl<T: PartialOrd> Pair<T>.
Voir la solution
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pomme", second: "poire" };
println!("{}", numbers.larger()); println!("{}", words.larger());}Sortie :
9pommeLes chaînes se comparent dans l’ordre alphabétique : "pomme" passe après "poire" (le m vient après le i), donc c’est elle la plus « grande ».
Récapitulatif
Section intitulée « Récapitulatif »- Les génériques permettent d’écrire une seule fonction ou struct qui marche avec plusieurs types, au lieu de dupliquer le code.
<T>est un paramètre de type ; les trait bounds (T: PartialOrd) limitent ce queTa le droit d’être.impl<T> Pair<T>ajoute des méthodes pour tous lesT;impl Pair<f64>en ajoute pour un seul type concret.- Le code générique est compilé en versions spécialisées pour chaque type : aucun coût à l’exécution.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel problème les génériques règlent-ils ?
2. Dans fn largest<T: PartialOrd + Copy>(...), à quoi sert PartialOrd + Copy ?
3. Les génériques coûtent-ils quelque chose à l'exécution par rapport à du code écrit à la main ?
4. Que vous permet une seule définition de struct Pair<T> ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Generic Data Types — The Rust Book (en anglais) : les génériques dans les fonctions, les structs, les enums et les méthodes, avec l’exemple
largest. - Generics — Rust by Example (en anglais) : de courts exemples exécutables de fonctions et de structs génériques, et leur syntaxe.
- Implementation — Rust by Example (en anglais) : plus de détails sur les blocs
impl<T>et sur les méthodes pour un seul type concret. - Bounds — Rust by Example (en anglais) : comment les trait bounds limitent ce que peut être un type générique, et pourquoi on en a besoin.
- Generic parameters — The Rust Reference (en anglais) : les règles précises, pour quand vous voulez tous les détails.