backend / rust
Closures et itérateurs
Explication
Ce que vous allez apprendre
- Distinguer les trois façons dont une closure capture son environnement :
Fn,FnMut,FnOnce - Utiliser
movepour forcer une capture par valeur, indispensable pour les threads - Comprendre pourquoi les itérateurs Rust sont paresseux
- Enchaîner
filter,map,take,foldpour des traitements de collection lisibles - Implémenter son propre itérateur en définissant seulement la méthode
next()
Dans quel contexte ?
Un développeur doit traiter une liste de plusieurs millions de commandes pour n'en garder que les impaires, les élever au carré, et sommer le résultat. Une boucle manuelle avec des variables intermédiaires ferait le travail, mais une chaîne d'itérateurs (filter().map().sum()) exprime exactement la même intention de façon plus déclarative — et compile généralement en un code tout aussi rapide, voire plus rapide grâce à l'inlining et à la fusion des étapes.
D'abord, il faut comprendre comment une closure capture son environnement
Une closure est une fonction anonyme qui peut utiliser des variables de son contexte environnant. Rust distingue trois façons de capturer ces variables, reflétées par trois traits différents que le compilateur choisit automatiquement selon l'usage réel fait dans le corps de la closure.
| Trait | Capture | Exemple d'usage |
|---|---|---|
Fn | Par référence immuable | Lire une variable capturée, sans la modifier |
FnMut | Par référence mutable | Modifier une variable capturée (comme un compteur) |
FnOnce | Par valeur (consomme) | Une closure qui ne peut être appelée qu'une seule fois |
Une fois cette distinction acquise, un mot-clé devient indispensable dans certains contextes
move force la closure à prendre l'ownership des variables capturées plutôt que de les emprunter. C'est absolument nécessaire dès qu'une closure est envoyée à un autre thread (vu dans une prochaine leçon) : le compilateur doit garantir que la donnée capturée ne sera pas libérée par le thread d'origine pendant que le nouveau thread l'utilise encore.
Prérequis
Il faut être à l'aise avec l'ownership et le borrowing (leçons précédentes) : la capture d'une closure applique exactement les mêmes règles que le passage de paramètres à une fonction classique.
Il reste une caractéristique fondamentale des itérateurs à intégrer : la paresse
nombres.iter().filter(...).map(...) ne calcule strictement rien tant qu'aucune méthode "consommatrice" (comme .collect(), .sum(), ou une boucle for) n'est appelée. Cette paresse permet au compilateur de fusionner toute la chaîne en une seule passe optimisée sur les données, plutôt que de créer une collection intermédiaire à chaque étape.
Piège fréquent
Écrire une longue chaîne d'itérateurs sans jamais appeler de méthode consommatrice à la fin ne produit aucun résultat ni aucune erreur visible immédiatement : rien ne s'exécute simplement. Vérifie toujours que ta chaîne se termine par .collect(), .sum(), .for_each() ou une utilisation équivalente qui déclenche réellement l'évaluation.
Enfin, implémenter son propre itérateur ne demande qu'une seule méthode
Un type qui implémente Iterator en ne définissant que fn next(&mut self) -> Option<Self::Item> reçoit gratuitement tout l'arsenal des méthodes de la stdlib (map, filter, fold, sum, take...) sans avoir à les réécrire. C'est un excellent exemple de la puissance des traits avec méthodes par défaut, vus dans la leçon précédente.
Bonne pratique
Préfère une chaîne d'itérateurs à une boucle manuelle dès que la logique s'y prête naturellement (filtrer, transformer, réduire) : le code Rust idiomatique privilégie ce style, généralement aussi rapide qu'une boucle écrite à la main, souvent plus lisible.
Maintenant que tu maîtrises closures et itérateurs, la prochaine leçon aborde des cas de propriété plus complexes que le simple ownership unique vu jusqu'ici : les smart pointers Box, Rc et RefCell.
Commandes & code
Closures et itérateurs
Les itérateurs Rust sont paresseux (lazy) et compilés en code aussi rapide qu'une boucle manuelle.
fn main() {
// Closure : fonction anonyme qui capture son environnement
let facteur = 3;
let multiplier = |x: i32| x * facteur; // capture facteur par reference (Fn)
println!("{}", multiplier(5));
// FnMut : closure qui modifie une variable capturee
let mut total = 0;
let mut ajouter = |x: i32| total += x;
ajouter(5);
ajouter(10);
println!("{}", total);
// FnOnce : closure qui consomme une valeur capturee (ne peut etre appelee qu'une fois)
let texte = String::from("valeur");
let consommer = move || println!("consomme: {}", texte); // move force la capture par valeur
consommer();
// Iterateurs : paresseux, ne font rien tant qu'on ne les consomme pas (collect, sum, for...)
let nombres = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let resultat: Vec<i32> = nombres
.iter()
.filter(|&&n| n % 2 == 0) // garde les pairs
.map(|&n| n * n) // eleve au carre
.take(3) // limite a 3 elements
.collect();
println!("{:?}", resultat);
// Chaine complexe et lisible, evaluee en UNE seule passe (fusion des iterateurs)
let somme_carres_impairs: i32 = nombres.iter()
.filter(|&&n| n % 2 != 0)
.map(|&n| n * n)
.sum();
println!("{}", somme_carres_impairs);
// enumerate, zip, chain : combinaisons courantes
for (i, n) in nombres.iter().enumerate().take(3) {
println!("index {} -> {}", i, n);
}
let noms = vec!["a", "b", "c"];
let ages = vec![10, 20, 30];
let paires: Vec<(&str, i32)> = noms.iter().copied().zip(ages.iter().copied()).collect();
println!("{:?}", paires);
// fold : reduction generique (equivalent de Reduce)
let produit = nombres.iter().fold(1, |acc, &n| acc * n);
println!("{}", produit);
// any / all : tests booleens sur une collection
println!("{}", nombres.iter().any(|&n| n > 9));
println!("{}", nombres.iter().all(|&n| n > 0));
// Implementer son propre iterateur
struct Compteur { valeur: u32, max: u32 }
impl Iterator for Compteur {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.valeur < self.max {
self.valeur += 1;
Some(self.valeur)
} else {
None
}
}
}
let compteur = Compteur { valeur: 0, max: 5 };
let total: u32 = compteur.sum();
println!("{}", total);
}Résumé
Fn,FnMut,FnOncedécrivent comment une closure capture son environnement (lecture, mutation, consommation).moveforce la capture par valeur — indispensable pour envoyer une closure à un autre thread.- Les itérateurs sont paresseux : rien ne s'exécute avant
collect,sum,for, etc. - Implémenter
Iterator(justenext()) donne accès gratuitement à toute la boîte à outils (map,filter,fold...).
Exercices pratiques
Mission : une chaine d'iterateurs qui n'affiche jamais rien
Objectif : Diagnostiquer une chaine d'iterateurs paresseuse jamais consommee et une closure qui ne peut pas capturer par move, puis les corriger.
Contexte
Un dev a ecrit ce code pour filtrer et logger les commandes superieures a 100 euros, mais rien ne s'affiche jamais a l'ecran, sans la moindre erreur de compilation :
fn main() {
let montants = vec![50, 120, 30, 200, 80];
montants.iter().filter(|&&m| m > 100).map(|&m| println!("grosse commande: {}", m));
}Separement, un autre dev veut envoyer une closure a un thread (sujet de la prochaine lecon) mais obtient une erreur de lifetime sur la variable capturee, qu'il ne comprend pas encore.