Les types génériques, les traits et les durées de vie
Tous les langages de programmation ont des outils pour gérer la duplication des concepts. En Rust, un de ces outils est la généricité : ce sont des représentations abstraites de types concrets ou d’autres propriétés. Nous pouvons exprimer le comportement des génériques, ou comment ils interagissent avec d’autres génériques, sans savoir ce qu’il y aura à leur place 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 que d’un type concret comme i32 ou String, de la même manière qu’une fonction prend 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é des types 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, nous allons voir comment définir nos propres types, fonctions et méthodes utilisant des types génériques !
Pour commencer, nous allons examiner comment construire une fonction pour réduire la duplication de code. Ensuite, nous utiliserons la même technique pour construire une fonction générique à partir de deux fonctions qui se distinguent uniquement par le type de leurs paramètres. Nous expliquerons aussi comment utiliser les types génériques dans les définitions de structures et d’énumérations.
Ensuite, vous apprendrez comment utiliser les traits pour définir un comportement de manière générique. Vous pouvez combiner les traits avec des types génériques pour contraindre un type générique à n’accepter que certains types qui ont un comportement particulier, et non pas accepter n’importe quel type.
Enfin, nous verrons les durées de vie, un genre de générique qui indique au compilateur comment les références s’articulent entre elles. Les durées de vie nous permettent de fournir au compilateur suffisamment d’informations au sujet des valeurs empruntées pour qu’il puisse garantir la validité des références dans plus de situations qu’il ne le pourrait sans notre aide.
Supprimer les doublons en construisant une fonction
Les génériques nous permettent de remplacer des types spécifiques par un substitut qui représente plusieurs types, afin d’éviter la duplication de code. Avant de plonger dans la syntaxe des génériques, nous allons regarder comment supprimer les doublons sans avoir recours aux types génériques en extrayant une fonction qui remplace des valeurs spécifiques par un emplacement réservé qui représente de multiples valeurs. Ensuite, nous allons appliquer cette technique pour construire une fonction générique ! En voyant comment reconnaître du code dupliqué que vous pouvez extraire dans une fonction, vous allez commencer à reconnaître du code dupliqué qui peut utiliser la généricité.
Nous allons commencer avec le petit programme de l’encart 10-1 qui trouve le nombre le plus grand dans une liste.
fn main() {
let liste_de_nombres = vec![34, 50, 25, 100, 65];
let mut le_plus_grand = &liste_de_nombres[0];
for nombre in &liste_de_nombres {
if nombre > le_plus_grand {
le_plus_grand = nombre;
}
}
println!("Le nombre le plus grand est {le_plus_grand}");
assert_eq!(*le_plus_grand, 100);
}
Nous stockons une liste de nombres entiers dans la variable liste_de_nombres et plaçons une référence vers le premier nombre de la liste dans une variable appellée le_plus_grand. Ensuite, nous parcourons tous les nombres dans la liste, et si le nombre courant est plus grand que le nombre stocké dans le_plus_grand, nous remplaçons le nombre dans cette variable. Cependant, si le nombre courant est plus petit ou égal au nombre le plus grand trouvé précédemment, la variable ne change pas, et le code passe au nombre suivant de la liste. Après avoir parcouru tous les nombres de la liste, le_plus_grand devrait stocker le plus grand nombre, qui est 100 dans notre cas.
Il nous a maintenant été demandé de trouver le nombre le plus grand dans deux listes de nombres différentes. Pour ce faire, nous pourrions choisir de dupliquer le code de l’encart 10-1 et suivre la même logique à deux endroits différents du programme, comme dans l’encart 10-2.
fn main() {
let liste_de_nombres = vec![34, 50, 25, 100, 65];
let mut le_plus_grand = &liste_de_nombres[0];
for nombre in &liste_de_nombres {
if nombre > le_plus_grand {
le_plus_grand = nombre;
}
}
println!("Le nombre le plus grand est {le_plus_grand}");
let liste_de_nombres = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut le_plus_grand = &liste_de_nombres[0];
for nombre in &liste_de_nombres {
if nombre > le_plus_grand {
le_plus_grand = nombre;
}
}
println!("Le nombre le plus grand est {le_plus_grand}");
}
Bien que ce code fonctionne, la duplication de code est fastidieuse et source d’erreurs. Nous devons aussi penser à mettre à jour le code à plusieurs endroits si nous souhaitons le modifier.
Pour éviter cette duplication, nous pouvons créer un niveau d’abstraction en définissant une fonction qui travaille avec n’importe quelle liste de nombres entiers qu’on lui passe comme un paramètre. Cette solution rend notre code plus clair et nous permet d’exprimer le concept de trouver le nombre le plus grand dans une liste de manière abstraite.
Dans l’encart 10-3, nous extrayons le code qui trouve le nombre le plus grand dans une fonction qui s’appelle le_plus_grand. Puis nous appellons cette fonction pour trouver le plus grand nombre dans les deux listes de l’encart 10-2. Nous pourrions aussi utiliser cette fonction sur n’importe quelle autre liste de valeurs i32 que nous pourrions rencontrer à l’avenir.
fn le_plus_grand(liste: &[i32]) -> &i32 {
let mut le_plus_grand = &liste[0];
for element in liste {
if element > le_plus_grand {
le_plus_grand = element;
}
}
le_plus_grand
}
fn main() {
let liste_de_nombres = vec![34, 50, 25, 100, 65];
let resultat = le_plus_grand(&liste_de_nombres);
println!("Le nombre le plus grand est {resultat}");
assert_eq!(*resultat, 100);
let liste_de_nombres = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let resultat = le_plus_grand(&liste_de_nombres);
println!("Le nombre le plus grand est {resultat}");
assert_eq!(*resultat, 6000);
}
La fonction le_plus_grand a un paramètre qui s’appelle liste, qui représente n’importe quelle slice concrète de valeurs i32 que nous pouvons passer à la fonction. Au final, lorsque nous appelons la fonction, le code s’exécute sur les valeurs précises que nous lui avons fournies.
En résumé, voici les étapes que nous avons suivies pour changer le code de l’encart 10-2 pour obtenir celui de l’encart 10-3 :
- Identification du code dupliqué.
- Extraction du code dupliqué dans le corps de la fonction et ajout de précisions sur les entrées et les valeurs de retour de ce code dans la signature de la fonction.
- Remplacement des deux instances du code dupliqué par des appels à la fonction.
Ensuite, nous allons utiliser les mêmes étapes avec la généricité pour réduire la duplication de code. De la même manière que le corps d’une fonction peut opérer sur une liste abstraite plutôt que sur des valeurs spécifiques, la généricité permet de travailler sur des types abstraits.
Par exemple, imaginons que nous ayons deux fonctions : une qui trouve l’élément le plus grand dans une slice de valeurs i32 et une qui trouve l’élément le plus grand dans une slice de valeurs char. Comment pourrions-nous éviter la duplication ? Voyons cela dès maintenant !