📍 Guide principal de la série : Apprendre Rust de zéro : le guide complet pour débuter. Ce tutoriel fait partie de la série « Rust de zéro ». Pour la vue d’ensemble, lisez d’abord le guide principal.
Assembler toutes les briques en un outil réel
Vous avez construit, tutoriel après tutoriel, les pièces de stock-cli : un type Produit, des méthodes, la gestion d’erreurs. Il manque deux choses pour en faire un vrai outil : une collection qui contient plusieurs produits et grandit au fil de l’eau, et la capacité de parcourir cette collection pour calculer, filtrer, agréger. C’est le rôle de Vec, des itérateurs et de HashMap. On y ajoutera deux concepts qui rendent le code réutilisable : les génériques et les traits.
Ce dernier tutoriel est le point d’orgue de la série : on assemble tout pour obtenir un outil en ligne de commande complet qui charge un inventaire, calcule sa valeur totale, liste les produits en stock faible, et compte les produits par état (en stock ou en rupture). À la fin, vous aurez un programme Rust qui ressemble à ce qu’on écrit en vrai, pas à un exercice jouet.
🎯 Ce que vous allez apprendre
- Stocker une liste dynamique avec
Vecet la parcourir avec des itérateurs (map,filter,sum). - Agréger des données avec
HashMapet son APIentry. - Écrire une fonction générique
<T>et définir un comportement partagé avec un trait. - Assembler un outil CLI complet qui lit ses arguments et affiche un rapport d’inventaire.
🛠️ Ce que vous allez construire
La version finale de stock-cli : un Vec<Produit> en mémoire, une valeur totale du stock calculée par itérateur, la liste des produits sous le seuil d’alerte, un comptage par état via HashMap, et un trait Resume qui donne à chaque produit une ligne d’affichage soignée.
Prérequis
- Avoir suivi les tutoriels sur les structs/enums et la gestion d’erreurs.
- Test express : si vous savez définir une
struct Produitavec une méthode, vous êtes prêt à l’assembler ici. - ⏱️ Temps estimé : ~50 minutes — c’est le plus complet, on construit l’outil entier.
Étape 1 — Vec : une liste qui grandit
Le tableau de taille fixe ne suffit plus : un inventaire s’agrandit. Vec<T> (vecteur) est un tableau dynamique, alloué sur le tas, qui peut accueillir un nombre variable d’éléments du même type. On le crée vide puis on y ajoute avec push, ou directement avec la macro vec! :
let mut inventaire: Vec<Produit> = Vec::new();
inventaire.push(Produit::new("Écran iPhone 11", 4500, 8));
inventaire.push(Produit::new("Batterie A10", 2000, 2));
inventaire.push(Produit::new("Chargeur USB-C", 1500, 0));
println!("{} produits référencés", inventaire.len());
La variable doit être mut pour qu’on puisse y ajouter des éléments. Vec gère seul son agrandissement : quand il manque de place, il réalloue une zone plus grande, de façon transparente. On accède à un élément par index (inventaire[0]), mais en pratique on parcourt rarement par index : on utilise les itérateurs, bien plus sûrs et expressifs.
Étape 2 — Les itérateurs : calculer sans boucle manuelle
Un itérateur produit les éléments d’une collection un par un, et offre une cascade de méthodes pour les transformer. Plutôt qu’une boucle for avec un accumulateur, on enchaîne des opérations lisibles. Calculons la valeur totale de l’inventaire :
let total: u32 = inventaire.iter()
.map(|p| p.valeur()) // chaque produit -> sa valeur (prix * quantité)
.sum(); // additionne le tout
println!("Valeur totale du stock : {total} FCFA");
iter() crée un itérateur qui emprunte chaque produit. map applique une fonction à chacun — ici la méthode valeur() via une closure |p| p.valeur() (une fonction anonyme dont p est le paramètre). sum consomme l’itérateur et additionne. Le tout en trois lignes déclaratives, sans variable mutable ni risque d’erreur d’index. Les itérateurs sont aussi paresseux : rien ne se calcule tant qu’une méthode finale (comme sum ou collect) ne déclenche le parcours.
Filtrer fonctionne pareil. Listons les produits sous le seuil d’alerte (moins de 3 unités), en collectant les noms dans un nouveau Vec :
let a_reapprovisionner: Vec<&str> = inventaire.iter()
.filter(|p| p.quantite < 3) // garde ceux en stock faible
.map(|p| p.nom.as_str()) // récupère le nom
.collect(); // rassemble dans un Vec
println!("À réapprovisionner : {a_reapprovisionner:?}");
filter ne garde que les éléments pour lesquels la closure renvoie true. collect rassemble le résultat dans une collection — ici un Vec<&str>, déduit par l’annotation de type. Ce style « itérateur » est idiomatique en Rust : plus court, plus sûr et souvent aussi rapide qu’une boucle écrite à la main.
✅ Point d’étape — Vous devez voir la valeur totale (par exemple 40000 FCFA) et la liste
["Batterie A10", "Chargeur USB-C"]. Sicollectse plaint d’un type ambigu, ajoutez l’annotationVec<&str>sur la variable :collecta besoin de savoir quelle collection produire.
Étape 3 — HashMap : agréger par clé
Pour compter les produits par état (en stock ou en rupture), on a besoin d’associer une clé (l’état) à une valeur (un compteur). C’est le rôle de HashMap<K, V>, une table de hachage. Son API entry rend le comptage élégant : elle insère une valeur par défaut si la clé est absente, puis renvoie une référence modifiable.
use std::collections::HashMap;
let mut compte: HashMap<&str, u32> = HashMap::new();
for p in &inventaire {
let etat = if p.quantite == 0 { "rupture" } else { "en stock" };
*compte.entry(etat).or_insert(0) += 1;
}
for (etat, n) in &compte {
println!("{etat} : {n} produit(s)");
}
La ligne clé est *compte.entry(etat).or_insert(0) += 1. entry cherche la clé ; or_insert(0) insère 0 si elle est absente et renvoie une référence mutable vers la valeur ; le * déréférence pour incrémenter. En une ligne, on a le « compter ou créer » que d’autres langages écrivent en trois. On importe HashMap avec use std::collections::HashMap; — il n’est pas dans le prélude automatique, contrairement à Vec.
Étape 4 — Génériques : écrire une fois, réutiliser partout
Imaginez une fonction qui renvoie le premier élément d’une liste, quel que soit son type. L’écrire pour Vec<Produit> puis la réécrire pour Vec<u32> serait absurde. Les génériques permettent d’écrire la logique une seule fois, valable pour tout type T :
fn premier<T>(liste: &[T]) -> Option<&T> {
liste.first() // renvoie Some(&premier) ou None si vide
}
fn main() {
let nombres = vec![10u32, 20, 30];
let noms = vec!["Écran", "Batterie"];
println!("{:?}", premier(&nombres)); // Some(10)
println!("{:?}", premier(&noms)); // Some("Écran")
}
Le <T> après le nom de la fonction déclare un type générique. premier fonctionne pour un slice de n’importe quel type, sans duplication. C’est le même mécanisme qui sous-tend Vec<T>, Option<T> et HashMap<K, V> : ces types sont génériques, et c’est pourquoi ils marchent avec vos propres structures sans rien réécrire.
Étape 5 — Les traits : définir un comportement partagé
Un trait décrit un comportement qu’un type peut implémenter — l’équivalent d’une interface. Définissons un trait Resume qui impose une méthode resume(), puis implémentons-le pour Produit :
trait Resume {
fn resume(&self) -> String;
}
impl Resume for Produit {
fn resume(&self) -> String {
format!("{} ({} en stock)", self.nom, self.quantite)
}
}
fn main() {
let p = Produit::new("Écran iPhone 11", 4500, 8);
println!("{}", p.resume()); // Écran iPhone 11 (8 en stock)
}
Le bloc trait déclare le contrat (la signature de resume), le bloc impl Resume for Produit le remplit. N’importe quel type peut implémenter Resume à sa façon, et on peut écrire des fonctions génériques qui acceptent « tout type qui implémente Resume ». C’est ainsi que Rust fait du polymorphisme sans héritage. D’ailleurs, les annotations #[derive(Debug)] que vous utilisez depuis le début ne sont que des implémentations automatiques de traits standard.
✅ Point d’étape —
p.resume()doit afficher « Écran iPhone 11 (8 en stock) ». Si le compilateur ditmethod not found, vérifiez que le blocimpl Resume for Produitest bien présent et que le trait est dans la portée.
Étape 6 — Assembler l’outil CLI complet
Réunissons tout. Un outil en ligne de commande lit ses arguments via std::env::args, qui renvoie un itérateur des arguments passés au programme. On accepte un seuil d’alerte optionnel, et on affiche un rapport complet :
use std::env;
fn main() {
// 1er argument après le nom du programme = seuil d'alerte (défaut 3)
let seuil: u32 = env::args().nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let inventaire = vec![
Produit::new("Écran iPhone 11", 4500, 8),
Produit::new("Batterie A10", 2000, 2),
Produit::new("Chargeur USB-C", 1500, 0),
];
let total: u32 = inventaire.iter().map(|p| p.valeur()).sum();
println!("=== Rapport stock-cli ===");
println!("Valeur totale : {total} FCFA");
println!("Seuil d'alerte : {seuil}");
println!("-- À réapprovisionner --");
for p in inventaire.iter().filter(|p| p.quantite < seuil) {
println!(" {}", p.resume());
}
}
env::args().nth(1) récupère le premier argument réel (l’index 0 est le nom du programme). and_then tente de le convertir en nombre, unwrap_or(3) retombe sur 3 si l’argument manque ou est invalide — une chaîne d’Option qui ne plante jamais. On lance avec cargo run -- 5 (le -- sépare les arguments de Cargo de ceux du programme) pour fixer le seuil à 5. L’outil affiche la valeur totale puis, grâce à filter et au trait resume(), la liste soignée des produits à réapprovisionner.
✅ Point d’étape —
cargo runaffiche le rapport avec le seuil 3 ;cargo run -- 5élargit la liste d’alerte. Si rien ne s’affiche sous « À réapprovisionner », c’est que tous vos stocks dépassent le seuil — baissez-le pour vérifier.
Une remarque pour relier ce tutoriel au précédent : dans cette version, l’inventaire est codé en dur dans le main. La suite logique, une fois ce socle maîtrisé, consiste à le charger depuis un fichier texte avec std::fs::read_to_string et à propager l’erreur avec l’opérateur ? — exactement la technique de gestion d’erreurs vue auparavant. Vous parseriez chaque ligne (par exemple nom;prix;quantite) en la découpant avec split(';'), puis en construisant un Produit par ligne, le tout rassemblé dans le Vec avec un collect. C’est l’enchaînement naturel de toutes les briques de la série : les collections tiennent les données, les itérateurs les transforment, et Result sécurise la lecture. À ce stade, stock-cli n’est plus un exercice mais un véritable petit logiciel, prêt à être étendu selon les besoins réels d’un atelier ou d’une boutique.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
cannot borrow `inventaire` as mutable |
push sur un Vec non mut |
Déclarer let mut inventaire |
type annotations needed sur collect |
Type de collection cible inconnu | Annoter : let v: Vec<_> = ....collect(); |
cannot find type HashMap |
Import manquant | Ajouter use std::collections::HashMap; |
method `resume` not found |
Trait non implémenté ou hors portée | Vérifier impl Resume for Produit |
| L’argument CLI est ignoré | Oubli du -- avec cargo run |
Lancer cargo run -- 5 |
🌍 Adaptation au contexte ouest-africain
Un outil comme stock-cli n’a rien d’un exercice abstrait : c’est exactement le genre de logiciel utile à un atelier de Pikine, une boutique de Bamako ou un dépôt de Cotonou. Compilé en --release, il devient un binaire unique de quelques méga-octets, sans dépendance à installer, qui démarre instantanément et tourne sur n’importe quelle machine — y compris un vieux portable ou un Raspberry Pi alimenté en cas de coupure. Pas de runtime à déployer, pas de base de données lourde : un fichier exécutable et un fichier texte d’inventaire suffisent.
C’est un atout commercial concret pour un développeur freelance de la sous-région : livrer un outil de gestion qui « juste marche » sur le matériel existant du client, sans frais d’hébergement mensuel ni connexion permanente. La sobriété de Rust — un seul binaire, mémoire prévisible, démarrage immédiat — colle aux réalités du terrain, là où des solutions gourmandes en serveur seraient coûteuses ou peu fiables.
✅ Récapitulatif
Vous avez assemblé un outil complet. Vous savez stocker une liste dynamique avec Vec, la parcourir et l’agréger avec les itérateurs (map, filter, sum, collect), compter par clé avec HashMap et son API entry, écrire une fonction générique <T>, et définir un comportement partagé avec un trait. Le tout converge dans un stock-cli qui lit ses arguments et produit un vrai rapport d’inventaire. Vous tenez là les fondations du Rust quotidien — il ne vous reste qu’à les approfondir sur vos propres projets. Le guide principal de la série vous indique les prochaines directions.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
Vec::new() / vec![...] |
Liste dynamique |
.push(x) |
Ajoute un élément |
.iter().map(...).sum() |
Transforme puis agrège |
.filter(...).collect() |
Filtre puis rassemble |
HashMap + .entry(k).or_insert(v) |
Compter / agréger par clé |
fn f<T>(...) |
Fonction générique |
trait T { ... } + impl T for X |
Comportement partagé |
env::args().nth(1) |
Lire un argument CLI |
💪 À vous de jouer
Ajoutez au rapport une ligne qui affiche le produit le plus cher de l’inventaire. Indice : l’itérateur possède une méthode max_by_key qui renvoie un Option de l’élément maximisant une clé.
Voir une solution
if let Some(p) = inventaire.iter().max_by_key(|p| p.prix) {
println!("Produit le plus cher : {} ({} FCFA)", p.nom, p.prix);
}
max_by_key parcourt l’itérateur et renvoie Some(&produit) ayant le plus grand prix, ou None si l’inventaire est vide. On déballe avec if let pour gérer proprement le cas vide.
Tutoriels frères
- Gérer les erreurs : Result, Option et l’opérateur ? — la robustesse à combiner avec les collections.
- Installer Rust et Cargo — pour repartir des bases si besoin.
Pour aller plus loin
- 🔝 Retour au guide principal : Apprendre Rust de zéro : le guide complet
- Pour le Rust orienté web et performance : Rust pour le web perf-critique : stack et tutoriels
- Chapitres « Common Collections », « Generic Types » et « Traits » du livre : doc.rust-lang.org/book
FAQ
Q : Quand utiliser un Vec plutôt qu’un tableau [T; N] ?
R : Le tableau a une taille fixe connue à la compilation. Le Vec grandit et rétrécit à l’exécution. Pour un inventaire qui évolue, c’est toujours Vec. Le tableau convient aux ensembles figés (jours de la semaine, par exemple).
Q : Les itérateurs sont-ils plus lents qu’une boucle for ?
R : Non, en général. Le compilateur les optimise au point qu’ils produisent souvent le même code machine qu’une boucle écrite à la main — c’est ce qu’on appelle des abstractions « à coût zéro ». Préférez le style itérateur pour la lisibilité.
Q : Quelle différence entre génériques et traits ?
R : Les génériques (<T>) permettent d’écrire du code valable pour plusieurs types. Les traits définissent un comportement qu’un type implémente. On les combine souvent : une fonction générique peut exiger que T implémente un trait donné.
Q : Comment lire les arguments d’un programme plus complexe ?
R : std::env::args suffit pour des cas simples. Dès que les options se multiplient (drapeaux, sous-commandes), la crate clap de l’écosystème gère l’analyse des arguments proprement. C’est l’étape naturelle quand stock-cli grandit.