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
Stringsans se tromper entre octets et caractères ; - compter avec une
HashMapet sa méthodeentry; - 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
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:?}");
}
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ération | Méthode | Remarque |
|---|---|---|
| Créer | Vec::new(), vec![a, b, c] | La variable doit être mut pour ajouter ou retirer |
| Ajouter à la fin | push(x) | |
| Lire | v[i], v.get(i) | v[i] panique hors limites, get renvoie un Option |
| Taille | len(), is_empty() | |
| Retirer | pop(), remove(i), retain(condition) | pop renvoie un Option ; remove décale les éléments suivants |
| Chercher | contains(&x), iter().position(…), iter().find(…) | |
| Trier | sort(), sort_by_key(…) | sort exige des éléments comparables |
La manière de parcourir décide de la propriété (chapitre 3) :
| Boucle | Chaque élément est | Après la boucle, le vecteur |
|---|---|---|
for x in &v | emprunté en lecture (&T) | est toujours utilisable |
for x in &mut v | emprunté en écriture (&mut T) ; on modifie avec *x = … | est toujours utilisable, modifié |
for x in v | dé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 :
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}");
}
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] :
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.
| Besoin | Méthode |
|---|---|
| Ajouter du texte | push_str("…"), push('c'), +=, format!(…) |
| Nettoyer | trim(), to_lowercase(), to_uppercase() |
| Chercher | contains("…"), starts_with, ends_with, find |
| Découper | split(';'), split_once(':'), lines(), split_whitespace() |
| Remplacer | replace("a", "b"), strip_prefix("…") |
| Convertir | parse::<u32>() vers un nombre ; to_string() vers une String |
| Compter | len() 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 :
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());
}
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 unOption<&V>: la clé peut manquer.map[clé]panique si elle manque ; réserve-le aux clés dont tu es sûr.insertrenvoie l'ancienne valeur s'il y en avait une, dans unOption.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.*… += 1incrémente cette valeur. C'est le motif standard pour compter des occurrences.- Une
HashMapne garantit aucun ordre de parcours, et l'ordre change d'une exécution à l'autre. Quand l'ordre compte (un affichage, un rapport), utilise uneBTreeMap, 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.
#[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");
}
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
| Méthode | Type | Rôle | Équivalent JavaScript |
|---|---|---|---|
filter(|x| condition) | Adaptateur | Garde les éléments qui vérifient la condition | filter |
map(|x| valeur) | Adaptateur | Transforme chaque élément | map |
enumerate() | Adaptateur | Ajoute la position : (0, x), (1, y)… | forEach((x, i) => …) |
take(n), skip(n) | Adaptateur | Garde les n premiers, ou les saute | slice |
sum(), count() | Consommateur | Additionne, compte | reduce, length |
collect() | Consommateur | Rassemble dans une collection (Vec, HashMap, String…) | (implicite) |
find(|x| …), position(…) | Consommateur | Premier élément qui convient, dans un Option | find, findIndex |
any(…), all(…) | Consommateur | Au moins un, tous | some, every |
max_by_key(…), min_by_key(…) | Consommateur | Plus grand ou plus petit selon une clé, dans un Option | (à la main) |
Trois points à comprendre :
- Les itérateurs sont paresseux :
filteretmapne 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. collecta besoin de connaître le type voulu :let anciens: Vec<&str> = …collect();. Sans l'annotation, il ne sait pas s'il doit construire unVec, uneHashMapou uneString.- Les fermetures capturent leur environnement :
|p| p.prix_ht <= budget_maxutilise la variablebudget_maxdé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
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}");
}
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 :
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
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 ?
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
avaut 6 : « Chloé » a 5 caractères, mais « é » occupe 2 octets.bvaut 5 : le nombre de caractères.cvaut « CHLOÉ » :to_uppercasegère les lettres accentuées.dvaut « éolhC » :rev()inverse l'ordre des caractères, etcollect::<String>()les rassemble. La notation::<String>(le « turbofish ») donne le type àcollectsans 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
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é ?