iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 8·30 min

Closures et itérateurs

Une closure est une fonction anonyme qui peut capturer les variables autour d'elle. Un itérateur est un objet qui produit des valeurs une par une. Combinés, ils donnent le style le plus idiomatique de Rust : des chaînes filter, map, collect qui se lisent comme une phrase, et qui compilent en une boucle aussi rapide qu'un for écrit à la main.

Les closures

fn main() {
    let double = |x: i32| x * 2;
    let ajouter = |a, b| a + b;          // types déduits au premier appel
    println!("{} {}", double(21), ajouter(2, 3));
 
    let seuil = 10;
    let au_dessus = |n: &i32| *n > seuil; // capture seuil
    let valeurs = vec![4, 12, 9, 30];
    let grands: Vec<_> = valeurs.into_iter().filter(au_dessus).collect();
    println!("{grands:?}"); // [12, 30]
}

La syntaxe : les paramètres entre barres verticales, puis une expression ou un bloc. Contrairement à une fn, une closure peut lire seuil, défini à l'extérieur.

Comment une closure capture

Une closure capture chaque variable de la façon la plus légère possible, selon ce qu'elle en fait :

Ce que fait la closureCaptureTrait implémenté
Lit la variablePar référence &Fn
Modifie la variablePar référence &mutFnMut
Consomme la variablePar valeur (déplacement)FnOnce
fn main() {
    let mut compteur = 0;
    let mut incrementer = || compteur += 1; // FnMut : modifie compteur
    incrementer();
    incrementer();
    println!("{compteur}"); // 2
 
    let nom = String::from("Ada");
    let consommer = move || nom;            // move : prend possession de nom
    let recupere = consommer();
    println!("{recupere}");
    // println!("{nom}"); // erreur : nom a été déplacé dans la closure
}

move force la capture par valeur. On l'utilise surtout quand la closure doit survivre à la fonction qui l'a créée : un thread, une tâche asynchrone, une closure renvoyée par une fonction.

Ces trois traits servent à typer une fonction qui reçoit une closure :

fn appliquer_deux_fois<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}
 
fn fabriquer_multiplicateur(facteur: i32) -> impl Fn(i32) -> i32 {
    move |x| x * facteur
}
 
fn main() {
    println!("{}", appliquer_deux_fois(|x| x + 3, 10)); // 16
    let triple = fabriquer_multiplicateur(3);
    println!("{}", triple(7));                          // 21
}

Le trait Iterator

Tout itérateur implémente un trait qui tient en une méthode :

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

next renvoie Some(valeur) tant qu'il en reste, puis None. Une boucle for ne fait rien d'autre qu'appeler next jusqu'à None.

Trois méthodes créent un itérateur à partir d'une collection, avec les trois modes d'accès habituels :

MéthodeProduitLa collection après
.iter()&TIntacte
.iter_mut()&mut TModifiée sur place
.into_iter()TConsommée

Adaptateurs et consommateurs

Un adaptateur transforme un itérateur en un autre : map, filter, take, skip, enumerate, zip, rev, chain, flat_map. Un consommateur le parcourt et produit un résultat : collect, sum, count, min, max, fold, any, all, find, position.

fn main() {
    let notes = vec![("Ada", 17), ("Linus", 9), ("Grace", 14), ("Alan", 11)];
 
    let admis: Vec<&str> = notes
        .iter()
        .filter(|(_, note)| *note >= 10)
        .map(|(nom, _)| *nom)
        .collect();
    println!("{admis:?}"); // ["Ada", "Grace", "Alan"]
 
    let moyenne = notes.iter().map(|(_, n)| *n as f64).sum::<f64>() / notes.len() as f64;
    println!("moyenne : {moyenne:.2}");
 
    let meilleur = notes.iter().max_by_key(|(_, n)| *n).unwrap();
    println!("meilleur : {}", meilleur.0);
 
    let tous_presents = notes.iter().all(|(_, n)| *n > 0);
    let premier_echec = notes.iter().find(|(_, n)| *n < 10);
    println!("{tous_presents} {premier_echec:?}");
}

Les adaptateurs sont paresseux : rien ne se passe tant qu'un consommateur ne réclame pas les valeurs. notes.iter().map(...) seul ne calcule rien, et le compilateur vous prévient d'ailleurs si vous l'oubliez.

collect, le couteau suisse

collect construit n'importe quelle collection, selon le type demandé :

use std::collections::{HashMap, HashSet};
 
fn main() {
    let mots = ["rust", "go", "rust", "zig"];
 
    let uniques: HashSet<_> = mots.iter().collect();
    let longueurs: HashMap<&str, usize> = mots.iter().map(|m| (*m, m.len())).collect();
    let phrase: String = mots.iter().map(|m| m.to_uppercase()).collect::<Vec<_>>().join("-");
 
    println!("{} uniques, {:?}, {}", uniques.len(), longueurs.get("zig"), phrase);
 
    // collecter des Result : s'arrête à la première erreur
    let nombres: Result<Vec<i32>, _> = ["1", "2", "x"].iter().map(|s| s.parse::<i32>()).collect();
    println!("{nombres:?}"); // Err(ParseIntError { kind: InvalidDigit })
}

La dernière forme est précieuse : un itérateur de Result se collecte en Result<Vec<_>, _>, qui vaut Ok avec tout le vecteur si tout a réussi, ou la première erreur sinon.

Quelques adaptateurs à connaître

fn main() {
    let a = [1, 2, 3];
    let b = ["un", "deux", "trois"];
 
    for (n, mot) in a.iter().zip(b.iter()) {
        print!("{n}={mot} ");
    }
    println!();
 
    let pairs: Vec<i32> = (1..=20).filter(|n| n % 2 == 0).skip(2).take(3).collect();
    println!("{pairs:?}"); // [6, 8, 10]
 
    let lettres: Vec<char> = ["ab", "cd"].iter().flat_map(|s| s.chars()).collect();
    println!("{lettres:?}"); // ['a', 'b', 'c', 'd']
 
    let produit = (1..=5).fold(1, |acc, n| acc * n);
    println!("{produit}"); // 120
 
    let groupes: Vec<Vec<i32>> = (1..=7).collect::<Vec<_>>().chunks(3).map(|c| c.to_vec()).collect();
    println!("{groupes:?}"); // [[1, 2, 3], [4, 5, 6], [7]]
}

Écrire son propre itérateur

struct Fibonacci {
    courant: u64,
    suivant: u64,
}
 
impl Iterator for Fibonacci {
    type Item = u64;
 
    fn next(&mut self) -> Option<u64> {
        let valeur = self.courant;
        self.courant = self.suivant;
        self.suivant += valeur;
        Some(valeur)
    }
}
 
fn fibonacci() -> Fibonacci {
    Fibonacci { courant: 0, suivant: 1 }
}
 
fn main() {
    let premiers: Vec<u64> = fibonacci().take(10).collect();
    println!("{premiers:?}");
 
    let pairs_sous_1000: u64 = fibonacci().take_while(|&n| n < 1000).filter(|n| n % 2 == 0).sum();
    println!("{pairs_sous_1000}"); // 798
}

En implémentant une seule méthode, next, on récupère gratuitement les dizaines d'adaptateurs du trait. L'itérateur est infini, et ce n'est pas un problème : la paresse fait qu'on ne calcule que ce que take ou take_while demandent.

Et la performance ?

On pourrait croire qu'une chaîne filter().map().sum() coûte plus cher qu'une boucle. C'est faux : le compilateur fusionne tout en une seule boucle, souvent même vectorisée. Les itérateurs sont une abstraction à coût nul, l'une des promesses centrales de Rust. Choisissez entre boucle et itérateur pour la lisibilité, pas pour la vitesse.

Ma règle : une transformation (filtrer, convertir, agréger) s'écrit en itérateur. Une boucle avec des effets de bord compliqués, des break multiples ou des états qui s'entremêlent reste un for.

Exercice

À partir d'un texte, affichez les trois mots de plus de trois lettres les plus fréquents, en ignorant la casse et la ponctuation.

use std::collections::HashMap;
 
fn main() {
    let texte = "Le Rust est rapide. Le Rust est sûr. Rust compile, Rust protège, rapide et sûr !";
 
    let mut frequences: HashMap<String, usize> = HashMap::new();
    texte
        .split(|c: char| !c.is_alphanumeric())
        .filter(|m| m.chars().count() > 3)
        .map(|m| m.to_lowercase())
        .for_each(|m| *frequences.entry(m).or_insert(0) += 1);
 
    let mut classement: Vec<_> = frequences.into_iter().collect();
    classement.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
 
    for (mot, n) in classement.iter().take(3) {
        println!("{mot} : {n}");
    }
}

chars().count() plutôt que len() : len compte les octets, et sûr en fait quatre pour trois lettres.


Closures et itérateurs donnent à Rust un style fonctionnel lisible, sans rien céder en vitesse. Le prochain chapitre sort du fichier unique : modules, crates, dépendances et tests, tout ce qu'il faut pour organiser un vrai projet.

Précédent
Durées de vie et pointeurs intelligents
Suivant
Modules, crates et tests

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main