Retour au cours

backend / rust

Collections : Vec et HashMap

Leçon 91 exercice

Explication

Ce que vous allez apprendre

  • Créer, remplir et parcourir un Vec<T>, le tableau dynamique par défaut de Rust
  • Distinguer l'indexation directe (v[i], qui panique) de .get() (sûr, retourne Option)
  • Manipuler des collections avec les méthodes fonctionnelles des itérateurs
  • Utiliser HashMap::entry().or_insert(), le pattern central pour "insérer si absent, sinon modifier"
  • Comprendre pourquoi l'ordre d'itération d'une HashMap n'est jamais garanti

Dans quel contexte ?

Un développeur écrit une fonction qui compte le nombre d'occurrences de chaque mot dans un texte, pour un outil d'analyse de logs. Sans connaître le pattern entry().or_insert(), il finirait par écrire un match verbeux vérifiant à chaque mot si la clé existe déjà avant de décider d'incrémenter ou d'insérer — alors qu'une seule ligne idiomatique fait exactement ce travail en Rust.

D'abord, Vec<T> est la structure de données la plus utilisée du langage

Vec::new() crée un tableau dynamique vide, capable de grandir via .push(). La macro vec![1, 2, 3] offre un raccourci pratique pour l'initialiser directement avec des valeurs. Contrairement à un tableau de taille fixe ([i32; 5]), un Vec peut changer de taille pendant l'exécution du programme.

Une fois cette structure de base acquise, il faut choisir entre deux façons d'accéder à un élément

L'indexation directe (nombres[10]) panique immédiatement si l'index dépasse la taille du vecteur — un comportement volontairement strict pour éviter un accès mémoire invalide silencieux. .get(10), à l'inverse, retourne Option<&T> : Some(valeur) si l'index existe, None sinon, sans jamais paniquer.

AccèsComportement hors limitesQuand l'utiliser
v[i]Panique immédiatementIndex dont on est absolument certain de la validité
v.get(i)Retourne None, aucun panicIndex potentiellement invalide, entrée utilisateur

Prérequis

Il faut être à l'aise avec le pattern matching (Option, leçon précédente) pour bien exploiter .get(), et avec les closures pour les méthodes fonctionnelles comme .filter()/.map().

Il reste un idiome extrêmement répandu dans le code Rust : les méthodes fonctionnelles

.iter().filter(...).map(...).collect() enchaîne des transformations sur une collection de façon lisible et déclarative. Ces itérateurs sont paresseux (rien ne s'exécute avant .collect() ou une autre méthode "consommatrice"), et ce sujet sera approfondi dans une prochaine leçon dédiée.

Ensuite, place à la seconde collection essentielle : HashMap<K, V>

Une HashMap associe des clés à des valeurs, comme un dictionnaire. Le pattern *scores.entry(cle).or_insert(0) += 1 est LE réflexe idiomatique pour "incrémenter un compteur associé à une clé, en le créant à zéro s'il n'existe pas encore" — une seule ligne remplace ce qui prendrait un match complet dans une approche plus manuelle.

Piège fréquent

Comme les maps en Go, l'ordre d'itération d'une HashMap Rust n'est jamais garanti et peut varier d'une exécution à l'autre. Si un ordre déterministe est nécessaire (pour un affichage ou un test reproductible), il faut trier explicitement les clés extraites, ou utiliser une BTreeMap qui maintient un ordre trié en permanence.

Enfin, un détail sur les structs stockées dans une collection

Bonne pratique

Pour compter des occurrences ou regrouper des valeurs par clé, pense toujours au pattern entry().or_insert() avant d'écrire un match manuel : c'est plus court, plus lisible, et c'est exactement ce que tout développeur Rust expérimenté reconnaîtra instantanément dans ton code.

Maintenant que tu maîtrises les deux collections centrales, la prochaine leçon aborde un sujet indissociable de tout code Rust réel : la gestion d'erreurs avec Result, Option et l'opérateur ?.

Commandes & code

Collections : Vec et HashMap

Les collections les plus utilisées de la bibliothèque standard, allouées sur le heap.

rust
use std::collections::HashMap;

fn main() {
    // Vec<T> : tableau dynamique, generique
    let mut nombres: Vec<i32> = Vec::new();
    nombres.push(1);
    nombres.push(2);
    nombres.push(3);

    let nombres2 = vec![1, 2, 3, 4, 5]; // macro pratique pour initialiser

    // Acces : indexation (panique si hors bornes) vs .get() (retourne Option, sur)
    println!("{}", nombres2[2]);
    match nombres2.get(10) {
        Some(v) => println!("valeur: {}", v),
        None => println!("index hors bornes"),
    }

    // Iteration
    for n in &nombres2 {
        print!("{} ", n);
    }
    println!();

    // Iteration mutable : modifier chaque element en place
    let mut carres = vec![1, 2, 3, 4];
    for n in &mut carres {
        *n *= *n;
    }
    println!("{:?}", carres);

    // Methodes fonctionnelles via iterateurs (tres idiomatique en Rust)
    let pairs: Vec<i32> = nombres2.iter().filter(|&&n| n % 2 == 0).copied().collect();
    let doubles: Vec<i32> = nombres2.iter().map(|n| n * 2).collect();
    let somme: i32 = nombres2.iter().sum();
    println!("{:?} {:?} {}", pairs, doubles, somme);

    // Vec de types heterogenes via enum ou trait object
    #[derive(Debug)]
    enum Valeur { Entier(i32), Texte(String) }
    let melange = vec![Valeur::Entier(1), Valeur::Texte("deux".to_string())];
    println!("{:?}", melange);

    // HashMap<K, V> : table de hachage
    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("Alice"), 90);
    scores.insert(String::from("Bob"), 85);

    // entry() : pattern idiomatique pour "inserer si absent, sinon modifier"
    scores.entry(String::from("Carla")).or_insert(0);
    *scores.entry(String::from("Alice")).or_insert(0) += 10; // Alice -> 100

    match scores.get("Bob") {
        Some(score) => println!("Bob: {}", score),
        None => println!("Bob non trouve"),
    }

    for (nom, score) in &scores { // ordre non garanti, comme en Go
        println!("{}: {}", nom, score);
    }

    // Compter les occurrences : pattern tres courant avec entry
    let mots = vec!["le", "chat", "le", "chien", "le"];
    let mut compteur: HashMap<&str, i32> = HashMap::new();
    for mot in mots {
        *compteur.entry(mot).or_insert(0) += 1;
    }
    println!("{:?}", compteur);
}

Résumé

  • Vec<T> est le tableau dynamique par défaut ; .get() est sûr, l'indexation [i] panique si hors bornes.
  • Les itérateurs (map, filter, sum, collect) sont l'idiome central de manipulation de collections.
  • HashMap::entry().or_insert() est LE pattern pour "insérer si absent, sinon modifier" en une expression.
  • L'ordre d'itération d'une HashMap n'est pas garanti (comme les maps Go).

Exercices pratiques

1 disponible
1

Mission : dashboard de logs qui plante en production

Objectif : Diagnostiquer un panic d'indexation en production et le corriger avec le pattern entry/or_insert et un acces securise.

Contexte

Un outil interne agrege les logs d'un serveur et affiche le nombre de requetes par code HTTP. Il fonctionne parfaitement en local, mais crashe en production avec index out of bounds des que le tableau de logs recus est plus court que prevu un jour de faible trafic :

rust
fn dernier_code(logs: &Vec<u16>) -> u16 {
    logs[logs.len()]
}

Une autre partie du meme outil compte les occurrences de chaque code HTTP avec une boucle et un match verbeux sur contains_key. Ton equipe te demande de corriger le crash et de simplifier le comptage.

Résoudre l’exercice →