Fermetures#
Les fermetures (closures) sont des fonctions anonymes capables de capturer des variables de leur environnement. Elles constituent, avec les itérateurs (chapitre 16), le pilier du style fonctionnel en Rust. Contrairement aux fonctions ordinaires, une fermeture peut accéder aux variables locales du bloc dans lequel elle est définie, ce qui en fait un outil expressif pour les transformations de données, les rappels (callbacks) et les stratégies paramétrées. Le système de traits (chapitre 13) et les génériques (chapitre 14) garantissent que cette flexibilité n’introduit aucune ambiguité sur la gestion de la mémoire.
Syntaxe des fermetures#
Définition 100 (Fermeture)
Une fermeture est une valeur anonyme appelable, déclarée par la syntaxe |paramètres| expression. Le compilateur infère les types des paramètres et du retour d’après le contexte d’utilisation. Si le corps comporte plusieurs instructions, on utilise des accolades : |paramètres| { instructions }.
Exemple 89 (Syntaxe de base)
Voir le code ci-dessous.
{
// Fermeture la plus simple : un seul paramètre, une expression
let carre = |x| x * x;
println!("3² = {}", carre(3));
// Types annotés explicitement
let addition: fn(i32, i32) -> i32 = |a, b| a + b;
println!("2 + 5 = {}", addition(2, 5));
// Corps multi-lignes avec accolades
let clamp = |x: i32, min: i32, max: i32| {
if x < min {
min
} else if x > max {
max
} else {
x
}
};
println!("clamp(15, 0, 10) = {}", clamp(15, 0, 10));
}
3² = 9
2 + 5 = 7
clamp(15, 0, 10) = 10
()
Remarque 80
Contrairement aux fonctions fn, les fermetures n’exigent pas d’annotations de type : le compilateur les déduit du contexte. Toutefois, une fois les types fixés lors du premier appel, ils sont figés.
let identite = |x| x;
let s = identite("bonjour"); // fixe le type à &str
let n = identite(42); // erreur : i32 != &str
// error[E0308]: mismatched types: expected `&str`, found integer
Capture de l’environnement#
La caractéristique distinctive des fermetures est leur capacité à capturer des variables du scope englobant. Le compilateur choisit le mode de capture le moins contraignant possible.
Définition 101 (Modes de capture)
Lorsqu’une fermeture utilise une variable de son environnement, le compilateur sélectionne automatiquement le mode de capture selon l’usage qu’en fait le corps de la fermeture :
Par référence partagée (
&T) — la variable est lue sans être modifiée.Par référence mutable (
&mut T) — la variable est modifiée.Par valeur (move,
T) — la possession de la variable est transférée dans la fermeture.
Exemple 90 (Les trois modes de capture)
Voir le code ci-dessous.
{
// 1. Capture par référence partagée : la fermeture lit `nom`
let nom = String::from("Rust");
let saluer = || println!("Bonjour, {} !", nom);
saluer();
println!("nom est toujours accessible : {}", nom);
}
Bonjour, Rust !
nom est toujours accessible : Rust
()
{
// 2. Capture par référence mutable : la fermeture modifie `compteur`
let mut compteur = 0;
let mut incrementer = || { compteur += 1; };
incrementer();
incrementer();
println!("compteur = {}", compteur);
}
{
// 3. Capture par valeur : la possession de `donnees` est transférée
let donnees = vec![1, 2, 3];
let consommer = || { let _s: i32 = donnees.into_iter().sum(); };
consommer();
// donnees n'est plus accessible ici
}
compteur = 2
()
Remarque 81
Le compilateur détermine le mode de capture de chaque variable individuellement. Une même fermeture peut capturer certaines variables par référence et d’autres par valeur.
Les traits Fn, FnMut, FnOnce#
Définition 102 (Hiérarchie Fn / FnMut / FnOnce)
Les trois traits de fermeture sont définis dans std::ops et forment une hiérarchie d’inclusion :
FnOnce— la fermeture peut être appelée au moins une fois. Elle peut consommer ses captures (transfert de possession). Toute fermeture implémenteFnOnce.FnMut— la fermeture peut être appelée plusieurs fois et peut modifier ses captures par référence mutable.FnMutimpliqueFnOnce.Fn— la fermeture peut être appelée plusieurs fois sans modifier ses captures (accès en lecture seule).FnimpliqueFnMut, qui impliqueFnOnce.
Proposition 25 (Détermination du trait)
Le compilateur attribue automatiquement le trait le plus général possible :
Si la fermeture ne capture rien ou ne lit ses captures que par référence partagée, elle implémente
Fn(et donc aussiFnMutetFnOnce).Si elle modifie ses captures par référence mutable, elle implémente
FnMut(etFnOnce) mais pasFn.Si elle consomme (déplace) au moins une capture, elle n’implémente que
FnOnce.
// Fn : lecture seule
fn appeler_fn(f: impl Fn()) {
f();
f(); // peut être appelée plusieurs fois
}
let message = String::from("salut");
appeler_fn(|| println!("{}", message));
salut
salut
// FnMut : modification des captures
fn appeler_fn_mut(mut f: impl FnMut()) {
f();
f();
}
let mut total = 0;
appeler_fn_mut(|| {
total += 10;
println!("total = {}", total);
});
total = 10
total = 20
// FnOnce : consommation d'une capture
fn appeler_fn_once(f: impl FnOnce() -> String) {
let resultat = f(); // un seul appel possible
println!("Résultat : {}", resultat);
}
let nom = String::from("Rust");
appeler_fn_once(|| {
// nom est déplacé dans la valeur de retour
format!("Bonjour depuis {} !", nom)
});
Résultat : Bonjour depuis Rust !
Le mot-clé move#
Définition 103 (Fermeture move)
Le mot-clé move placé avant les paramètres d’une fermeture force la capture de toutes les variables par valeur (transfert de possession), indépendamment de ce que le corps de la fermeture exigerait. La syntaxe est move |paramètres| expression.
Exemple 91 (move et threads)
Un thread peut s’exécuter plus longtemps que le scope qui l’a lancé. Sans move, la fermeture tenterait de capturer par référence une variable locale qui pourrait être détruite avant la fin du thread.
use std::thread;
let noms = vec!["Alice", "Bob", "Charlie"];
// Sans move, le compilateur refuserait : noms pourrait être détruit
// avant la fin du thread.
let handle = thread::spawn(move || {
for nom in &noms {
println!("Bonjour, {} !", nom);
}
});
handle.join().unwrap();
Bonjour, Alice !
Bonjour, Bob !
Bonjour, Charlie !
Remarque 82
Pour les types Copy (entiers, booléens, etc.), move effectue une copie plutôt qu’un transfert de possession. La variable originale reste donc utilisable après la création de la fermeture.
{
let x = 42; // i32 est Copy
let f = move || println!("x = {}", x);
f();
println!("x est toujours accessible : {}", x);
}
x = 42
x est toujours accessible : 42
()
Fermetures comme paramètres#
Définition 104 (Passer une fermeture en paramètre)
Trois syntaxes permettent de recevoir une fermeture en paramètre :
impl Fn(...)— dispatch statique par monomorphisation. Le compilateur génère une version spécialisée de la fonction pour chaque fermeture distincte. C’est la forme la plus performante et la plus idiomatique.&dyn Fn(...)— dispatch dynamique via une référence à un objet-trait. Utile pour stocker plusieurs fermetures de types différents derrière un même type.Box<dyn Fn(...)>— dispatch dynamique avec possession. Permet de stocker la fermeture sur le tas et de la conserver au-delà du scope courant.
Exemple 92 (Les trois approches)
Voir le code ci-dessous.
{
// 1. impl Fn — dispatch statique (monomorphisation)
fn appliquer_statique(valeur: i32, f: impl Fn(i32) -> i32) -> i32 {
f(valeur)
}
// 2. &dyn Fn — dispatch dynamique par référence
fn appliquer_dynamique(valeur: i32, f: &dyn Fn(i32) -> i32) -> i32 {
f(valeur)
}
// 3. Box<dyn Fn> — dispatch dynamique avec possession
fn appliquer_boxed(valeur: i32, f: Box<dyn Fn(i32) -> i32>) -> i32 {
f(valeur)
}
println!("statique : {}", appliquer_statique(5, |x| x * 2));
println!("dynamique : {}", appliquer_dynamique(5, &|x| x * 2));
println!("boxed : {}", appliquer_boxed(5, Box::new(|x| x * 2)));
}
statique : 10
dynamique : 10
boxed : 10
()
Remarque 83
En pratique, impl Fn(...) est la forme à privilégier. On ne recourt au dispatch dynamique que lorsque l’on doit stocker des fermetures de types différents dans une même collection.
Retourner des fermetures#
Chaque fermeture possède un type anonyme unique : on ne peut pas écrire directement le type de retour. Deux solutions existent.
Définition 105 (Retourner une fermeture)
impl Fn(...)en position de retour — le compilateur connait le type concret, mais l’appelant le manipule comme un type opaque. Fonctionne lorsque la fonction retourne un seul type de fermeture.Box<dyn Fn(...)>— nécessaire lorsque la fonction peut retourner des fermetures de types différents selon une condition à l’exécution.
Exemple 93 (impl Fn en retour)
Voir le code ci-dessous.
{
fn creer_additionneur(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let plus_cinq = creer_additionneur(5);
let plus_dix = creer_additionneur(10);
println!("3 + 5 = {}", plus_cinq(3));
println!("3 + 10 = {}", plus_dix(3));
}
3 + 5 = 8
3 + 10 = 13
()
Exemple 94 (Box<dyn Fn> en retour)
Lorsque la branche choisie à l’exécution détermine la fermeture retournée, impl Fn ne suffit pas car les branches produisent des types distincts. Box<dyn Fn> unifie ces types.
fn creer_operation(doubler: bool) -> Box<dyn Fn(i32) -> i32> {
if doubler {
Box::new(|x| x * 2)
} else {
Box::new(|x| x + 1)
}
}
let op = creer_operation(true);
println!("op(7) = {}", op(7));
let op2 = creer_operation(false);
println!("op2(7) = {}", op2(7));
op(7) = 14
op2(7) = 8
Idiomes fonctionnels avec les itérateurs#
Les fermetures prennent toute leur dimension combinées aux adaptateurs d’itérateurs du chapitre 16 : map, filter, fold et leurs variantes.
Exemple 95 (map, filter, fold)
Voir le code ci-dessous.
{
let nombres = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map : transformer chaque élément
let carres: Vec<i32> = nombres.iter().map(|&x| x * x).collect();
println!("Carrés : {:?}", carres);
// filter : ne garder que les éléments satisfaisant un prédicat
let pairs: Vec<&i32> = nombres.iter().filter(|&&x| x % 2 == 0).collect();
println!("Pairs : {:?}", pairs);
// fold : accumuler une valeur à partir des éléments
let somme = nombres.iter().fold(0, |acc, &x| acc + x);
println!("Somme : {}", somme);
}
Carrés : [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Pairs : [2, 4, 6, 8, 10]
Somme : 55
()
Comparaison impérative / fonctionnelle#
Comparons les deux approches sur un même problème : calculer la somme des carrés des nombres pairs.
Exemple 96 (Style impératif vs fonctionnel)
Voir le code ci-dessous.
let nombres = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// --- Style impératif ---
let mut somme_imp = 0;
for &n in &nombres {
if n % 2 == 0 {
somme_imp += n * n;
}
}
println!("Impératif : {}", somme_imp);
// --- Style fonctionnel ---
let somme_fn: i32 = nombres.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.sum();
println!("Fonctionnel : {}", somme_fn);
Impératif : 220
Fonctionnel : 220
Remarque 84
Grace à la monomorphisation et à l’inlining, le compilateur produit un code machine équivalent pour les deux versions. Le style fonctionnel n’introduit aucun surcout : c’est une abstraction à cout zéro (zero-cost abstraction).
Enchainer les adaptateurs#
Les pipelines d’itérateurs peuvent enchainer un nombre arbitraire d’adaptateurs. L’évaluation est paresseuse : aucun élément intermédiaire n’est alloué en mémoire.
let phrase = "les fermetures en rust sont puissantes";
let resultat: String = phrase
.split_whitespace()
.filter(|mot| mot.len() > 3)
.map(|mot| {
let mut c = mot.chars();
match c.next() {
None => String::new(),
Some(p) => p.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ");
println!("{}", resultat);
Fermetures Rust Sont Puissantes
Résumé#
Les fermetures enrichissent Rust d’un style fonctionnel sans compromettre les garanties de sécurité ni les performances :
La syntaxe
|params| exproffre une notation légère avec inférence de types.La capture de l’environnement est gérée automatiquement selon le mode le moins contraignant.
Les traits
Fn,FnMutetFnOnceformalisent la hiérarchie des capacités d’une fermeture (chapitres 13 et 14).Le mot-clé
moveforce la capture par valeur, indispensable pour les threads et les retours de fermetures.Les fermetures se passent en paramètre via
impl Fn(...)oudyn Fn(...), et se retournent viaimpl Fn(...)ouBox<dyn Fn(...)>.Combinées aux itérateurs (chapitre 16), elles permettent d’écrire des pipelines déclaratifs et performants.