Les types génériques, les traits et les durées de vie
Chaque langage de programmation dispose d’outils pour gérer efficacement la duplication de concepts. En Rust, l’un de ces outils est la généricité : des substituts abstraits pour des types concrets ou d’autres propriétés. Nous pouvons exprimer le comportement des génériques ou la manière dont ils interagissent entre eux sans savoir ce qui les remplacera lors de la compilation et de l’exécution du code.
Les fonctions peuvent prendre des paramètres d’un type générique, plutôt qu’un type concret comme i32 ou String, de la même manière qu’elles prennent des paramètres avec des valeurs inconnues pour exécuter le même code sur plusieurs valeurs concrètes. En fait, nous avons déjà utilisé les génériques au chapitre 6 avec Option<T>, au chapitre 8 avec Vec<T> et HashMap<K, V>, et au chapitre 9 avec Result<T, E>. Dans ce chapitre, vous découvrirez comment définir vos propres types, fonctions et méthodes avec des génériques !
D’abord, nous reverrons comment extraire une fonction pour réduire la duplication de code. Nous utiliserons ensuite la même technique pour créer une fonction générique à partir de deux fonctions qui ne diffèrent que par les types de leurs paramètres. Nous expliquerons aussi comment utiliser les types génériques dans les définitions de structs et d’énumérations.
Ensuite, vous apprendrez à utiliser les traits pour définir un comportement de manière générique. Vous pouvez combiner les traits avec les types génériques pour contraindre un type générique à n’accepter que les types qui ont un comportement particulier, plutôt que n’importe quel type.
Enfin, nous aborderons les durées de vie (lifetimes) : une variété de génériques qui donnent au compilateur des informations sur la manière dont les références sont liées entre elles. Les durées de vie nous permettent de donner au compilateur suffisamment d’informations sur les valeurs empruntées pour qu’il puisse garantir que les références seront valides dans plus de situations qu’il ne le pourrait sans notre aide.
Éliminer la duplication en extrayant une fonction
Les génériques nous permettent de remplacer des types spécifiques par un espace réservé qui représente plusieurs types afin d’éliminer la duplication de code. Avant de plonger dans la syntaxe des génériques, voyons d’abord comment éliminer la duplication d’une manière qui n’implique pas de types génériques, en extrayant une fonction qui remplace des valeurs spécifiques par un espace réservé représentant plusieurs valeurs. Ensuite, nous appliquerons la même technique pour extraire une fonction générique ! En apprenant à reconnaître le code dupliqué que vous pouvez extraire dans une fonction, vous commencerez à reconnaître le code dupliqué qui peut utiliser des génériques.
Nous commencerons par le court programme de l’encart 10-1 qui trouve le plus grand nombre dans une liste.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
assert_eq!(*largest, 100);
}
Nous stockons une liste d’entiers dans la variable number_list et plaçons une référence vers le premier nombre de la liste dans une variable nommée largest. Nous itérons ensuite sur tous les nombres de la liste, et si le nombre actuel est supérieur au nombre stocké dans largest, nous remplaçons la référence dans cette variable. En revanche, si le nombre actuel est inférieur ou égal au plus grand nombre rencontré jusqu’à présent, la variable ne change pas, et le code passe au nombre suivant dans la liste. Après avoir examiné tous les nombres de la liste, largest devrait référencer le plus grand nombre, qui dans ce cas est 100.
On nous demande maintenant de trouver le plus grand nombre dans deux listes de nombres différentes. Pour ce faire, nous pouvons choisir de dupliquer le code de l’encart 10-1 et utiliser la même logique à deux endroits différents du programme, comme le montre l’encart 10-2.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
Bien que ce code fonctionne, dupliquer du code est fastidieux et source d’erreurs. Nous devons aussi nous souvenir de mettre à jour le code à plusieurs endroits lorsque nous voulons le modifier.
Pour éliminer cette duplication, nous allons créer une abstraction en définissant une fonction qui opère sur n’importe quelle liste d’entiers passée en paramètre. Cette solution rend notre code plus clair et nous permet d’exprimer de manière abstraite le concept de recherche du plus grand nombre dans une liste.
Dans l’encart 10-3, nous extrayons le code qui trouve le plus grand nombre dans une fonction nommée largest. Ensuite, nous appelons la fonction pour trouver le plus grand nombre dans les deux listes de l’encart 10-2. Nous pourrions aussi utiliser la fonction sur n’importe quelle autre liste de valeurs i32 que nous pourrions avoir à l’avenir.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
La fonction largest à un paramètre appelé list, qui représente n’importe quelle slice concrète de valeurs i32 que nous pourrions passer à la fonction. En conséquence, lorsque nous appelons la fonction, le code s’exécute sur les valeurs spécifiques que nous lui passons.
En résumé, voici les étapes que nous avons suivies pour transformer le code de l’encart 10-2 en l’encart 10-3 :
- Identifier le code dupliqué.
- Extraire le code dupliqué dans le corps de la fonction, et spécifier les entrées et valeurs de retour de ce code dans la signature de la fonction.
- Mettre à jour les deux instances de code dupliqué pour appeler la fonction à la place.
Ensuite, nous utiliserons ces mêmes étapes avec les génériques pour réduire la duplication de code. De la même manière que le corps de la fonction peut opérer sur une list abstraite plutôt que sur des valeurs spécifiques, les génériques permettent au code d’opérer sur des types abstraits.
Par exemple, supposons que nous ayons deux fonctions : l’une qui trouve le plus grand élément dans une slice de valeurs i32 et l’autre qui trouve le plus grand élément dans une slice de valeurs char. Comment éliminerions-nous cette duplication ? Découvrons-le !