Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 622 minIntermédiaire

Vec, String, HashMap et itérateurs

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • ajouter, lire, trier et retirer des éléments d'un Vec ;
  • manipuler une String sans se tromper entre octets et caractères ;
  • compter avec une HashMap et sa méthode entry ;
  • remplacer une boucle par une chaîne d'itérateurs.

4 exercices corrigés1 schéma

Les collections stockent un nombre variable de valeurs sur le tas : Vec<T> est une liste qui grandit, String un texte modifiable et HashMap<K, V> un dictionnaire qui associe des clés à des valeurs. Pour les parcourir, Rust propose les itérateurs : des chaînes de méthodes comme filter, map et sum, qui remplacent la plupart des boucles. Ce chapitre les applique à l'inventaire de Sorbier Logistique.

Vec : la liste qui grandit

rust
fn main() {
    let mut numeros: Vec<String> = Vec::new();
    numeros.push(String::from("PC-0002"));
    numeros.push(String::from("PC-0001"));
    numeros.push(String::from("PC-0007"));

    let prix_ht = vec![64900, 89900, 72900]; // la macro vec! remplit d'avance

    println!("{} postes, le premier est {}", numeros.len(), numeros[0]);
    println!("{:?} {:?}", numeros.get(2), numeros.get(5));

    numeros.sort();
    println!("Triés : {numeros:?}");

    // Parcourir sans prendre possession : &
    for numero in &numeros {
        print!("{numero} ");
    }
    println!();

    // Modifier chaque élément : &mut
    let mut prix_remises = prix_ht.clone();
    for prix in &mut prix_remises {
        *prix = *prix * 90 / 100;
    }
    println!("{prix_ht:?} → {prix_remises:?}");

    let retire = numeros.remove(0);
    println!("Retiré : {retire}, reste {numeros:?}");
    println!("Contient PC-0007 ? {}", numeros.contains(&String::from("PC-0007")));

    numeros.retain(|n| n != "PC-0007"); // garde ceux qui vérifient la condition
    let dernier = numeros.pop(); // Option : le vecteur peut être vide
    println!("Dernier : {dernier:?}, reste {numeros:?}");
}
text
3 postes, le premier est PC-0002
Some("PC-0007") None
Triés : ["PC-0001", "PC-0002", "PC-0007"]
PC-0001 PC-0002 PC-0007 
[64900, 89900, 72900] → [58410, 80910, 65610]
Retiré : PC-0001, reste ["PC-0002", "PC-0007"]
Contient PC-0007 ? true
Dernier : Some("PC-0002"), reste []
OpérationMéthodeRemarque
CréerVec::new(), vec![a, b, c]La variable doit être mut pour ajouter ou retirer
Ajouter à la finpush(x)
Lirev[i], v.get(i)v[i] panique hors limites, get renvoie un Option
Taillelen(), is_empty()
Retirerpop(), remove(i), retain(condition)pop renvoie un Option ; remove décale les éléments suivants
Cherchercontains(&x), iter().position(…), iter().find(…)
Triersort(), sort_by_key(…)sort exige des éléments comparables

La manière de parcourir décide de la propriété (chapitre 3) :

BoucleChaque élément estAprès la boucle, le vecteur
for x in &vemprunté en lecture (&T)est toujours utilisable
for x in &mut vemprunté en écriture (&mut T) ; on modifie avec *x = …est toujours utilisable, modifié
for x in vdéplacé dans x (T)n'existe plus

L'étoile de *prix = *prix * 90 / 100 déréférence : elle désigne la valeur sur laquelle pointe la référence prix, pour la lire et la remplacer.

String et UTF-8

Une String est un Vec d'octets qui contient forcément du texte UTF-8 valide. Les opérations courantes :

rust
fn main() {
    let mut message = String::from("Poste PC-0001");
    message.push_str(" affecté à ");
    message.push('J');
    message += "ulie";
    println!("{message}");

    let nom = "Hélène";
    println!("{} octets, {} caractères", nom.len(), nom.chars().count());
    println!("Initiale : {:?}", nom.chars().next());
    println!("{}", nom.to_uppercase());

    let ligne = "  PC-0002;fixe;HP;64900;2020;stock  ";
    let champs: Vec<&str> = ligne.trim().split(';').collect();
    println!("{champs:?}");
    println!("{}", ligne.contains("fixe"));
    println!("{}", "affecte:Karim Morel".replace("affecte:", ""));

    let annee: i32 = champs[4].parse().unwrap();
    let resume = format!("{} acheté en {annee}", champs[0]);
    println!("{resume}");
}
text
Poste PC-0001 affecté à Julie
8 octets, 6 caractères
Initiale : Some('H')
HÉLÈNE
["PC-0002", "fixe", "HP", "64900", "2020", "stock"]
true
Karim Morel
PC-0002 acheté en 2020

« Hélène » compte 6 caractères mais 8 octets : chaque « è » ou « é » occupe deux octets en UTF-8. len() renvoie des octets. C'est pour cela que Rust interdit nom[0] :

text
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src/main.rs:3:24
  |
3 |     let initiale = nom[0];
  |                        ^ string indices are ranges of `usize`
  |
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`
  = note: you can use `.chars().nth()` or `.bytes().nth()`

Que renverrait nom[1] ? Le premier octet du « é », qui n'est pas un caractère. Plutôt que de deviner, Rust te demande de choisir : chars() pour les caractères, bytes() pour les octets.

BesoinMéthode
Ajouter du textepush_str("…"), push('c'), +=, format!(…)
Nettoyertrim(), to_lowercase(), to_uppercase()
Cherchercontains("…"), starts_with, ends_with, find
Découpersplit(';'), split_once(':'), lines(), split_whitespace()
Remplacerreplace("a", "b"), strip_prefix("…")
Convertirparse::<u32>() vers un nombre ; to_string() vers une String
Compterlen() en octets, chars().count() en caractères

HashMap : associer une clé à une valeur

Une HashMap<K, V> associe des clés uniques à des valeurs, comme un Dictionary en C# ou un objet Map en JavaScript. Elle n'est pas importée par défaut :

rust
use std::collections::{BTreeMap, HashMap};

fn main() {
    // Qui utilise quel poste ?
    let mut affectations: HashMap<String, String> = HashMap::new();
    affectations.insert(String::from("PC-0001"), String::from("Julie Lefèvre"));
    affectations.insert(String::from("PC-0002"), String::from("Karim Morel"));

    match affectations.get("PC-0001") {
        Some(nom) => println!("PC-0001 → {nom}"),
        None => println!("PC-0001 n'est pas affecté"),
    }
    println!("{:?}", affectations.get("PC-0099"));

    // insert remplace l'ancienne valeur et la renvoie
    let ancien = affectations.insert(String::from("PC-0002"), String::from("Camille Roux"));
    println!("Avant : {ancien:?}, maintenant : {}", affectations["PC-0002"]);

    // Compter avec entry : le motif à connaître
    let marques = ["Dell", "HP", "Lenovo", "HP", "Dell", "Lenovo", "Dell"];
    let mut par_marque: HashMap<&str, u32> = HashMap::new();
    for marque in marques {
        *par_marque.entry(marque).or_insert(0) += 1;
    }
    println!("Dell : {}", par_marque["Dell"]);

    // BTreeMap garde les clés triées : affichage stable
    let trie: BTreeMap<&str, u32> = par_marque.into_iter().collect();
    for (marque, nombre) in &trie {
        println!("{marque:<7} {nombre}");
    }

    affectations.remove("PC-0001");
    println!("{} affectation(s)", affectations.len());
}
text
PC-0001 → Julie Lefèvre
None
Avant : Some("Karim Morel"), maintenant : Camille Roux
Dell : 3
Dell    3
HP      2
Lenovo  2
1 affectation(s)
  • get(clé) renvoie un Option<&V> : la clé peut manquer. map[clé] panique si elle manque ; réserve-le aux clés dont tu es sûr.
  • insert renvoie l'ancienne valeur s'il y en avait une, dans un Option.
  • entry(clé).or_insert(0) renvoie une référence mutable vers la valeur de la clé, en la créant à 0 si elle n'existe pas. *… += 1 incrémente cette valeur. C'est le motif standard pour compter des occurrences.
  • Une HashMap ne garantit aucun ordre de parcours, et l'ordre change d'une exécution à l'autre. Quand l'ordre compte (un affichage, un rapport), utilise une BTreeMap, qui garde ses clés triées. {marque:<7} aligne le texte à gauche sur 7 caractères.

Itérateurs et fermetures

Un itérateur produit les éléments d'une collection un par un. On l'obtient avec iter(), puis on enchaîne des adaptateurs qui transforment le flux, et on termine par un consommateur qui produit le résultat. Les petites fonctions passées en paramètre, écrites |p| …, sont des fermetures (closures) : des fonctions anonymes, comme les fonctions fléchées de JavaScript.

rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum TypePoste {
    Fixe,
    Portable,
    Serveur,
}

#[derive(Debug)]
struct Poste {
    numero: String,
    type_poste: TypePoste,
    prix_ht: u32,
    annee_achat: i32,
}

fn poste(numero: &str, type_poste: TypePoste, prix_ht: u32, annee_achat: i32) -> Poste {
    Poste { numero: numero.to_string(), type_poste, prix_ht, annee_achat }
}

fn main() {
    let parc = vec![
        poste("PC-0001", TypePoste::Portable, 89900, 2023),
        poste("PC-0002", TypePoste::Fixe, 64900, 2020),
        poste("PC-0003", TypePoste::Portable, 104900, 2024),
        poste("PC-0005", TypePoste::Serveur, 349000, 2021),
        poste("PC-0006", TypePoste::Portable, 99900, 2021),
    ];

    // Valeur des portables : filtrer, transformer, additionner
    let valeur_portables: u64 = parc
        .iter()
        .filter(|p| p.type_poste == TypePoste::Portable)
        .map(|p| u64::from(p.prix_ht))
        .sum();
    println!("Portables : {valeur_portables} centimes");

    // Les numéros des postes de 5 ans ou plus en 2026
    let anciens: Vec<&str> = parc
        .iter()
        .filter(|p| 2026 - p.annee_achat >= 5)
        .map(|p| p.numero.as_str())
        .collect();
    println!("À renouveler : {anciens:?}");

    let nombre_fixes = parc.iter().filter(|p| p.type_poste == TypePoste::Fixe).count();
    let plus_cher = parc.iter().max_by_key(|p| p.prix_ht);
    let tous_recents = parc.iter().all(|p| p.annee_achat >= 2020);
    let un_serveur = parc.iter().any(|p| p.type_poste == TypePoste::Serveur);
    println!("{nombre_fixes} fixe(s), {tous_recents}, {un_serveur}");
    if let Some(p) = plus_cher {
        println!("Le plus cher : {} ({} centimes)", p.numero, p.prix_ht);
    }

    for (rang, p) in parc.iter().enumerate().take(2) {
        println!("{} : {}", rang + 1, p.numero);
    }

    // Une fermeture peut capturer une variable de son environnement
    let budget_max = 100000;
    let abordables = parc.iter().filter(|p| p.prix_ht <= budget_max).count();
    println!("{abordables} postes à {budget_max} centimes ou moins");
}
text
Portables : 294700 centimes
À renouveler : ["PC-0002", "PC-0005", "PC-0006"]
1 fixe(s), true, true
Le plus cher : PC-0005 (349000 centimes)
1 : PC-0001
2 : PC-0002
3 postes à 100000 centimes ou moins
parc.iter()un par un.filter(…)les portables.map(…)leur prix.sum()le totalPC-0001 portablePC-0002 fixePC-0003 portablePC-0005 serveurPC-0006 portable××PC-0001PC-0003PC-00068990010490099900294700
Une chaîne d'itérateurs : iter produit, filter écarte, map transforme, sum consomme
MéthodeTypeRôleÉquivalent JavaScript
filter(|x| condition)AdaptateurGarde les éléments qui vérifient la conditionfilter
map(|x| valeur)AdaptateurTransforme chaque élémentmap
enumerate()AdaptateurAjoute la position : (0, x), (1, y)…forEach((x, i) => …)
take(n), skip(n)AdaptateurGarde les n premiers, ou les sauteslice
sum(), count()ConsommateurAdditionne, comptereduce, length
collect()ConsommateurRassemble dans une collection (Vec, HashMap, String…)(implicite)
find(|x| …), position(…)ConsommateurPremier élément qui convient, dans un Optionfind, findIndex
any(…), all(…)ConsommateurAu moins un, toussome, every
max_by_key(…), min_by_key(…)ConsommateurPlus grand ou plus petit selon une clé, dans un Option(à la main)

Trois points à comprendre :

  • Les itérateurs sont paresseux : filter et map ne font rien tant qu'un consommateur (sum, collect, count…) ne réclame pas les valeurs. Une chaîne sans consommateur ne s'exécute pas, et le compilateur t'en avertit.
  • collect a besoin de connaître le type voulu : let anciens: Vec<&str> = …collect();. Sans l'annotation, il ne sait pas s'il doit construire un Vec, une HashMap ou une String.
  • Les fermetures capturent leur environnement : |p| p.prix_ht <= budget_max utilise la variable budget_max déclarée à l'extérieur, sans qu'on la passe en paramètre.

Une chaîne d'itérateurs est aussi rapide qu'une boucle écrite à la main : le compilateur produit le même code machine. On parle d'abstraction à coût nul. Choisis donc la forme la plus lisible : les itérateurs disent ce que tu calcules, la boucle dit comment.

Exercices

Exercice 1 sur 4 Le budget par type Réussi À revoir

À partir du vecteur parc de la section « Itérateurs et fermetures », écris une boucle qui remplit une HashMap<String, u64> donnant la valeur totale des postes de chaque type (utilise format!("{:?}", p.type_poste) comme clé). Affiche-la dans l'ordre alphabétique des types.

Voir la réponseMasquer la réponse de l'exercice 1
rust
use std::collections::{BTreeMap, HashMap};

// … enum TypePoste, struct Poste, fn poste et le vecteur parc du chapitre …

let mut par_type: HashMap<String, u64> = HashMap::new();
for p in &parc {
    *par_type.entry(format!("{:?}", p.type_poste)).or_insert(0) += u64::from(p.prix_ht);
}
let trie: BTreeMap<String, u64> = par_type.into_iter().collect();
for (type_poste, total) in &trie {
    println!("{type_poste} : {total}");
}
text
Fixe : 64900
Portable : 294700
Serveur : 349000

Le passage par une BTreeMap donne l'ordre alphabétique ; parcourir directement la HashMap donnerait un ordre imprévisible. On pourrait aussi construire directement une BTreeMap avec entry : elle a la même méthode.

Tu avais trouvé ?

Exercice 2 sur 4 De la boucle aux itérateurs Réussi À revoir

Réécris cette fonction avec une chaîne d'itérateurs, sans mut :

rust
fn numeros_dell(postes: &[(String, String)]) -> Vec<String> {
    let mut resultat = Vec::new();
    for (numero, marque) in postes {
        if marque == "Dell" {
            resultat.push(numero.clone());
        }
    }
    resultat
}
Voir la réponseMasquer la réponse de l'exercice 2
rust
fn numeros_dell(postes: &[(String, String)]) -> Vec<String> {
    postes
        .iter()
        .filter(|(_, marque)| marque == "Dell")
        .map(|(numero, _)| numero.clone())
        .collect()
}

Les fermetures peuvent déstructurer le tuple dans leurs paramètres : |(_, marque)|. Le type de retour de la fonction indique à collect qu'il doit produire un Vec<String>.

Tu avais trouvé ?

Exercice 3 sur 4 Octets et caractères Réussi À revoir

Que valent a, b, c et d ?

rust
let nom = String::from("Chloé");
let a = nom.len();
let b = nom.chars().count();
let c = nom.to_uppercase();
let d = nom.chars().rev().collect::<String>();
Voir la réponseMasquer la réponse de l'exercice 3
  • a vaut 6 : « Chloé » a 5 caractères, mais « é » occupe 2 octets.
  • b vaut 5 : le nombre de caractères.
  • c vaut « CHLOÉ » : to_uppercase gère les lettres accentuées.
  • d vaut « éolhC » : rev() inverse l'ordre des caractères, et collect::<String>() les rassemble. La notation ::<String> (le « turbofish ») donne le type à collect sans annotation sur la variable.

Tu avais trouvé ?

Exercice 4 sur 4 Le poste le plus ancien Réussi À revoir

Avec iter() et min_by_key, trouve dans le vecteur parc le poste le plus ancien et affiche son numéro et son année. Que renvoie min_by_key sur un vecteur vide ?

Voir la réponseMasquer la réponse de l'exercice 4
rust
match parc.iter().min_by_key(|p| p.annee_achat) {
    Some(p) => println!("Le plus ancien : {} ({})", p.numero, p.annee_achat),
    None => println!("Parc vide"),
}

Le programme affiche « Le plus ancien : PC-0002 (2020) ». Sur un vecteur vide, min_by_key renvoie None : il n'y a pas de plus petit élément, et le type Option oblige à traiter ce cas.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Traits et génériques : les interfaces de RustRevoir le sommaire du cours