Compter des données avec HashMap

RustBeginner
Pratiquer maintenant

Introduction

Un vecteur organise les valeurs selon leur position numérique. Une HashMap<K, V> associe au contraire chaque clé unique de type K à une valeur de type V, ce qui la rend utile pour les compteurs, les paramètres et autres données indexées par des clés.

Vous allez créer une petite table de comptage de mots, récupérer et remplacer un compteur, utiliser l’API des entrées pour des mots existants et absents, puis parcourir toutes les valeurs sans dépendre de l’ordre de parcours des clés, qui n’est pas spécifié.

Insérer et récupérer des valeurs associées à des clés

Dans cette étape, vous allez insérer deux paires clé-valeur et récupérer un compteur en toute sécurité.

Accédez au projet et ouvrez le fichier source :

cd /home/labex/project/word-counts
nano src/main.rs

Le chemin préparé std::collections::HashMap identifie HashMap dans le module des collections de la bibliothèque standard de Rust. La ligne use attribue à ce chemin long le nom local abrégé HashMap. Considérez pour le moment cette ligne comme un import préparé ; les chemins et les imports seront étudiés en détail dans les modules ultérieurs du Lab. HashMap<String, u32> stocke des clés de type chaîne possédées et des compteurs entiers non signés. Remplacez le commentaire de l’étape 1a par :

    let _ = counts.insert(String::from("rust"), 1);
    let _ = counts.insert(String::from("safe"), 1);

insert renvoie toute valeur précédemment stockée pour cette clé. Ces clés sont nouvelles, donc le résultat est None. L’expression let _ = ...; évalue l’opération et ignore volontairement son résultat. Ce caractère générique _ seul est différent d’un nom tel que _returned_id : le caractère générique seul ne crée pas de liaison que vous pourriez réutiliser plus tard.

Remplacez le commentaire de l’étape 1b par :

    match counts.get("rust") {
        Some(count) => println!("Rust count: {count}"),
        None => println!("Rust count: missing"),
    }

get accepte le texte emprunté "rust" et renvoie un Option<&u32> sans retirer la clé ni le compteur de la table. Enregistrez le fichier et quittez l’éditeur, puis vérifiez et exécutez le programme :

cargo check
cargo run --quiet
Unique words: 2
Rust count: 1

La première ligne prouve que les clés sont uniques ; la deuxième confirme que la recherche a trouvé le compteur stocké.

Remplacer un compteur existant

Dans cette étape, vous allez utiliser insert avec une clé existante et examiner la valeur remplacée.

Ouvrez le fichier source :

nano src/main.rs

Remplacez le commentaire de l’étape 2 par :

    let previous = counts.insert(String::from("rust"), 2);
    println!("Previous rust count: {previous:?}");

Une table ne conserve qu’une seule valeur par clé. La nouvelle valeur 2 remplace 1, et la valeur renvoyée Some(1) rend ce remplacement observable. Enregistrez le fichier et quittez l’éditeur, puis exécutez le programme :

cargo run --quiet

La nouvelle ligne doit être :

Previous rust count: Some(1)

Le nombre de mots uniques reste égal à deux, car un remplacement ne crée pas de nouvelle clé.

Mettre à jour les compteurs avec l’API des entrées

Dans cette étape, vous allez incrémenter un mot existant et un mot absent avec la même API.

entry(key).or_insert(default) renvoie une référence mutable vers la valeur existante associée à la clé, ou insère la valeur par défaut et renvoie une référence mutable vers celle-ci. Ouvrez le fichier source :

nano src/main.rs

Remplacez le commentaire de l’étape 3 par :

    *counts.entry(String::from("safe")).or_insert(0) += 1;
    *counts.entry(String::from("fast")).or_insert(0) += 1;
    println!("Safe count: {}", counts["safe"]);
    println!("Fast count: {}", counts["fast"]);

Lisez cette mise à jour compacte de l’intérieur vers l’extérieur. Tout d’abord, entry(key) sélectionne l’emplacement associé à la clé, qu’il soit occupé ou vacant. Ensuite, or_insert(0) garantit qu’une valeur existe et renvoie une référence mutable, &mut u32, vers cette valeur.

L’opérateur * placé au début accède au u32 situé derrière cette référence. Enfin, += 1 ajoute un au compteur et enregistre le nouveau résultat dans la table.

Ainsi, safe passe à 2, tandis que fast, qui était absent, commence à zéro et passe à 1. Les crochets permettent ensuite de lire une valeur à partir de sa clé. L’indexation d’une table provoque une panique si la clé est absente ; elle est sûre ici uniquement parce que les deux appels précédents à entry garantissent l’existence des deux clés. Continuez à utiliser get lorsqu’une clé peut être absente.

Enregistrez le fichier et quittez l’éditeur, puis exécutez le programme :

cargo run --quiet

Les dernières lignes doivent maintenant être :

Safe count: 2
Fast count: 1

Une seule opération a géré les deux cas : clé présente et clé absente.

Parcourir les valeurs sans dépendre de l’ordre des clés

Dans cette étape, vous allez parcourir tous les compteurs stockés et calculer un total indépendant de l’ordre de parcours.

Les tables de hachage ne garantissent pas un ordre stable de parcours des clés. Pour vérifier simplement le résultat, il ne faut donc pas s’attendre à ce que les clés s’affichent dans un ordre particulier. La méthode values() parcourt chaque valeur stockée, ce qui suffit pour calculer un total.

Ouvrez le fichier source et remplacez le commentaire de l’étape 4 :

nano src/main.rs
    let mut total = 0;
    for count in counts.values() {
        total += count;
    }
    println!("Total occurrences: {total}");

Ici, count est une référence partagée, et += peut additionner l’entier référencé. Enregistrez le fichier et quittez l’éditeur, puis vérifiez et exécutez le programme :

cargo check
cargo run --quiet

La dernière ligne doit être :

Total occurrences: 5

Le total combine rust = 2, safe = 2 et fast = 1 sans dépendre de leur ordre de parcours.

Résumé

Vous avez inséré, récupéré, remplacé et incrémenté des compteurs associés à des clés, puis parcouru toutes les valeurs de la table sans supposer que l’ordre des clés était stable.