Réutiliser des structures de données avec les génériques

RustBeginner
Pratiquer maintenant

Introduction

Les types concrets sont utiles lorsque les données ont toujours la même forme. En revanche, répéter des types presque identiques pour les nombres et le texte rend un programme plus difficile à maintenir. Vous avez déjà utilisé des types génériques standard tels que Option<u32>, Vec<String>, HashMap<String, u32> et Result<T, E>. Dans cet atelier, vous allez définir vous-même un type générique. Les génériques Rust permettent à une même définition de fonctionner avec plusieurs types concrets, tout en laissant le compilateur vérifier chaque utilisation.

Dans cet atelier, vous allez rendre générique une structure Pair préparée, puis généraliser un petit utilitaire qui emprunte des données. Le programme principal et les exemples de données sont déjà écrits. Votre travail se limite donc aux quelques emplacements de type nécessaires pour permettre la réutilisation.

Rendre la structure Pair générique

Dans cette étape, vous allez remplacer le type fixe i32 de la structure par un paramètre de type afin que la même définition puisse contenir des scores ou des noms d’itinéraires.

Accédez au paquet Cargo préparé :

cd /home/labex/project/pair-demo

Examinez le fichier source court. La commande sed -n affiche la plage de lignes demandée sans modifier le fichier :

sed -n '1,120p' src/main.rs

La valeur scores contient des entiers, tandis que routes contient des valeurs String possédées. La définition actuelle de Pair fixe les deux champs à i32 ; elle ne peut donc pas représenter ces deux types de valeurs. L’attribut préparé #[derive(Debug)] générerait un formatage de débogage pour le type, mais le programme affiché n’utilise pas ce formatage ; cet attribut ne fait pas partie de la tâche sur les génériques.

Un type générique introduit un paramètre fictif tel que T entre chevrons. Chaque Pair construite possède toujours un type concret : le compilateur déduit Pair<i32> pour scores et Pair<String> pour routes.

Ouvrez le fichier source dans Nano :

nano src/main.rs

Effectuez ces trois petites modifications en haut du fichier :

  • Remplacez struct Pair { par struct Pair<T> {.
  • Remplacez le type de left, de i32 par T.
  • Remplacez le type de right, de i32 par T.

La structure terminée est la suivante :

struct Pair<T> {
    left: T,
    right: T,
}

Ici, les deux champs utilisent le même T. Une paire ne peut donc pas mélanger accidentellement des types de champs incompatibles. Enregistrez avec Ctrl+O, appuyez sur Entrée, puis quittez avec Ctrl+X.

Demandez à Cargo de vérifier les types du paquet :

cargo check

Une ligne se terminant par Finished signifie que le compilateur a accepté Pair<i32> et Pair<String>. Exécutez maintenant le programme :

cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82

Les deux premières lignes prouvent qu’une seule définition de structure peut maintenant stocker deux types concrets différents. L’utilitaire first existant n’accepte encore que Pair<i32> ; vous allez le généraliser à l’étape suivante.

Généraliser un utilitaire qui emprunte des données

Dans cette étape, vous allez donner à la fonction first son propre paramètre de type afin qu’elle puisse emprunter n’importe quel Pair<T> et renvoyer une référence vers sa première valeur.

Examinez l’utilitaire actuel en haut du fichier :

sed -n '1,30p' src/main.rs

Son entrée Pair<i32> et sa sortie &i32 le rendent spécifique aux scores. Une fonction générique déclare <T> après son nom, puis utilise ce même T partout où les types d’entrée et de sortie associés apparaissent.

Ouvrez le fichier :

nano src/main.rs

Modifiez uniquement la déclaration de la fonction, en la remplaçant :

fn first(pair: &Pair<i32>) -> &i32 {

par :

fn first<T>(pair: &Pair<T>) -> &T {

La fonction emprunte la paire et renvoie un champ emprunté. Elle ne déplace donc pas la valeur stockée et n’en crée pas de copie. Rust peut déduire T à partir de chaque argument ; les appels n’ont pas besoin d’indiquer explicitement le type.

Remplacez ensuite // FIRST_ROUTE_REPORT par un appel qui prouve que l’utilitaire fonctionne également avec du texte :

    println!("first route: {}", first(&routes));

Enregistrez le fichier et quittez Nano. Vérifiez ensuite les types et exécutez le programme terminé :

cargo check
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
first route: north

Les deux lignes first proviennent de la même fonction générique. Rust déduit T = i32 pour l’appel avec le score et T = String pour l’appel avec l’itinéraire, puis vérifie chaque appel avec le type concret correspondant. La réutilisation ne supprime donc pas la vérification statique des types.

Résumé

Vous avez remplacé une paire concrète par Pair<T>, l’avez utilisée avec des entiers et des chaînes de caractères, puis avez généralisé un utilitaire qui emprunte des données, sans introduire de traits ni dupliquer les implémentations.