Aller au contenu

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.

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.

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 :

47
2.8
▶ Essayez par vous-même

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

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 10
bonjour monde
▶ Essayez par vous-même

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

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

Il y a ici deux sortes de blocs impl :

  • impl<T> Pair<T> veut dire « ces méthodes existent pour une Pair de n’importe quel type ». Lisez le premier <T> comme « pour tout type T ». Sans lui, Rust croirait que T est 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 pour Pair<f64>. C’est pour ça que average peut utiliser + et / 2.0. Essayez d’appeler words.average() : le compilateur vous dira que cette méthode n’existe pas pour Pair<&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.

É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 :

3
r
▶ Essayez par vous-même

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 :

9
pomme
▶ Essayez par vous-même

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

  • 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 que T a le droit d’être.
  • impl<T> Pair<T> ajoute des méthodes pour tous les T ; 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

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