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é. La généricité permet de remplacer des types concrets ou d'autres propriétés par des paramètres abstraits appelés génériques. Lorsque nous écrivons du code, 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.

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, les fonctions peuvent prendre des paramètres d'un type générique plutôt que d'un type concret comme i32 ou String. 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 uniquement à des 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 d'emprunter des valeurs dans différentes situations tout en donnant les éléments au compilateur pour vérifier que les références sont toujours valides.

Supprimer les doublons en construisant une fonction

Avant de plonger dans la syntaxe des génériques, nous allons regarder comment supprimer les doublons, sans utiliser de types génériques, en construisant une fonction. Ensuite, nous allons appliquer cette technique pour construire une fonction générique ! De la même manière que vous détectez du code dupliqué pour l'extraire dans une fonction, vous allez commencer à reconnaître du code dupliqué qui peut utiliser la généricité.

Imaginons un petit programme qui trouve le nombre le plus grand dans une liste, comme dans l'encart 10-1.

Fichier: src/main.rs

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);
}

Encart 10-1 : le code pour trouver le nombre le plus grand dans une liste de nombres

Ce code stocke une liste de nombres entiers dans la variable liste_de_nombres et place le premier nombre de la liste dans une variable qui s'appelle le_plus_grand. Ensuite, il parcourt tous les nombres dans la liste, et si le nombre courant est plus grand que le nombre stocké dans le_plus_grand, il remplace 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.

Pour trouver le nombre le plus grand dans deux listes de nombres différentes, nous pourrions 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.

Fichier : src/main.rs

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);
}

Encart 10-2 : le code pour trouver le plus grand nombre dans deux listes de nombres

Bien que ce code fonctionne, la duplication de code est fastidieuse et source d'erreurs. Nous devons aussi 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 donne en 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 avons extrait le code qui trouve le nombre le plus grand dans une fonction qui s'appelle le_plus_grand. Contrairement au code de l'encart 10-1, qui pouvait trouver le nombre le plus grand dans une seule liste en particulier, ce programme peut trouver le nombre le plus grand dans deux listes différentes.

Fichier : src/main.rs

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);
}

Encart 10-3 : du code abstrait qui trouve le plus grand nombre dans deux listes

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. Mais ne nous préoccupons pas de la syntaxe de la boucle for pour l'instant. Ici, nous n'utilisons pas une référence vers un i32, nous destructurons via le filtrage par motif chaque &i32 afin que la boucle for utilise cet element en tant que i32 dans le corps de la boucle. Nous parlerons plus en détails du filtrage par motif au chapitre 18.

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 :

  1. Identification du code dupliqué.
  2. 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.
  3. 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 différentes façons. 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 !