Les closures en Rust
Une closure (fonction anonyme) est une petite fonction sans nom que vous pouvez ranger dans une variable et faire circuler dans votre programme. Ce qui la distingue d’une fonction classique, c’est qu’elle peut « capturer » des variables de la portée où elle a été créée.
La syntaxe de base
Section intitulée « La syntaxe de base »fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Sortie :
6Les barres verticales |x: i32| remplacent les parenthèses qu’utiliserait une fonction classique. Comparez les deux côte à côte :
fn add_one_fn(x: i32) -> i32 { x + 1 } // une fonction classiquelet add_one_closure = |x: i32| x + 1; // la même chose sous forme de closureRust peut souvent deviner aussi le type des paramètres et du retour, donc vous verrez souvent des closures encore plus courtes :
let add_one = |x| x + 1;Une closure sans paramètre utilise des barres vides : let say_hi = || println!("salut");, et on l’appelle avec say_hi().
Les closures sur plusieurs lignes
Section intitulée « Les closures sur plusieurs lignes »Pour plus d’une expression, entourez le corps d’accolades, exactement comme pour une fonction :
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "pair" } else { "impair" }; format!("{n} est {kind}") };
println!("{}", describe(7));}Sortie :
7 est impairformat! fonctionne comme println!, mais au lieu d’afficher le texte, il le renvoie sous forme de String. Ici, cette String est la dernière expression entre les accolades : c’est donc ce que renvoie la closure.
Capturer des variables de la portée environnante
Section intitulée « Capturer des variables de la portée environnante »Voici ce qu’une fonction classique ne sait pas faire : une closure peut utiliser les variables de l’endroit où elle a été définie, sans que vous ayez à les lui passer explicitement.
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}Sortie :
9040discount n’est pas un paramètre de la closure : elle se trouve simplement dans la portée de main, et la closure va la chercher pour l’utiliser directement. Une fn classique ne peut pas faire ça ; il faudrait lui passer discount en argument.
Les closures qui modifient les variables capturées
Section intitulée « Les closures qui modifient les variables capturées »Une closure peut aussi modifier une variable qu’elle capture, à condition que cette variable soit mut. Il y a une règle de plus : la closure elle-même doit aussi être rangée dans une variable mut. L’appeler modifie quelque chose, et Rust veut que cela se voie dans votre code.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("le compteur vaut maintenant {count}"); };
increment(); increment(); increment();
println!("valeur finale : {count}");}Sortie :
le compteur vaut maintenant 1le compteur vaut maintenant 2le compteur vaut maintenant 3valeur finale : 3Si vous oubliez le mut sur increment, le compilateur vous dit exactement ce qui ne va pas : calling increment requires mutable binding due to mutable borrow of count. Autrement dit, appeler increment exige une variable mutable, parce que la closure emprunte count en mode mutable.
Il y a un piège à connaître. Tant que la closure peut encore être appelée, elle a le droit de modifier count, donc personne d’autre n’a le droit d’y toucher. Si vous ajoutez println!("{count}"); entre deux appels à increment(), vous obtenez error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable : impossible de lire count pendant qu’elle est empruntée pour être modifiée. Après le dernier appel à increment(), la closure a fini son travail et count est de nouveau à vous. C’est pour ça que la ligne valeur finale fonctionne. Vous découvrirez toute l’histoire derrière cette règle à la page Borrowing.
Parfois, une closure doit prendre l’ownership (la possession) complète de ce qu’elle capture, par exemple quand on la confie à un autre thread. On écrit alors move avant les barres : move || { ... }. Vous le verrez en action à la page Concurrence.
Où l’on rencontre les closures en pratique
Section intitulée « Où l’on rencontre les closures en pratique »Vous utiliserez rarement une closure toute seule. Là où elles sont vraiment utiles, c’est pour passer un comportement personnalisé à une autre fonction, en particulier aux méthodes d’itérateurs que vous découvrirez à la page Itérateurs :
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Sortie :
[2, 4, 6, 8, 10]Ici, |n| n * 2 est une closure passée directement à .map(). C’est ce schéma (une petite closure qui pilote une méthode comme .map() ou .filter()) que vous utiliserez vraiment au quotidien.
Exercices
Section intitulée « Exercices »1. Des prix TTC
Section intitulée « 1. Des prix TTC »Créez une variable tax_percent qui vaut 20. Puis écrivez une closure with_tax qui prend un prix et renvoie ce prix taxes comprises, en utilisant la variable capturée tax_percent. Affichez le résultat pour les prix 50 et 120. Indice : la taxe vaut price * tax_percent / 100.
Voir la solution
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 avec les taxes donne {}", with_tax(50)); println!("120 avec les taxes donne {}", with_tax(120));}Sortie :
50 avec les taxes donne 60120 avec les taxes donne 144On ne passe jamais tax_percent en paramètre. La closure la capture depuis main, ce qu’une fn classique ne pourrait justement pas faire.
2. Un générateur d’identifiants
Section intitulée « 2. Un générateur d’identifiants »Écrivez une closure next_id qui renvoie 1 la première fois que vous l’appelez, 2 la deuxième fois, 3 la troisième, et ainsi de suite. Appelez-la trois fois et affichez chaque identifiant. Indice : gardez un compteur mut en dehors de la closure, ajoutez-lui 1 à l’intérieur, et faites du compteur la dernière expression de la closure pour qu’il soit renvoyé.
Voir la solution
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("le premier utilisateur reçoit l'id {}", next_id()); println!("le deuxième utilisateur reçoit l'id {}", next_id()); println!("le troisième utilisateur reçoit l'id {}", next_id());}Sortie :
le premier utilisateur reçoit l'id 1le deuxième utilisateur reçoit l'id 2le troisième utilisateur reçoit l'id 3last_id et next_id ont tous les deux besoin de mut : le premier parce que sa valeur change, le second parce qu’appeler la closure modifie ce qu’elle a capturé.
Récapitulatif
Section intitulée « Récapitulatif »- Une closure est une petite fonction anonyme :
|paramètres| expression. - Les types sont souvent devinés par Rust, donc une closure est généralement plus courte que la
fnéquivalente. - Contrairement à une fonction classique, une closure peut capturer des variables de la portée où elle a été définie.
- Une closure qui modifie une variable capturée doit être rangée dans une variable
mut, et elle emprunte cette variable tant qu’on l’utilise. - Les closures sont surtout utiles quand on les passe à d’autres fonctions, en particulier aux méthodes d’itérateurs comme
.map().
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quels symboles une closure utilise-t-elle à la place des parenthèses d'une fonction pour ses paramètres ?
2. Que peut faire une closure qu'une fn classique ne peut pas faire ?
3. D'après cette page, où utilise-t-on le plus souvent les closures en pratique ?
4. Une closure fait count += 1 sur une variable capturée. De quoi d'autre avez-vous besoin ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Closures — The Rust Book (en anglais) : le chapitre officiel sur les closures, l’inférence de types et les traits
Fn. - Capturing References or Moving Ownership — The Rust Book (en anglais) : les trois façons dont une closure peut capturer une variable, et quand utiliser
move. - Closures — Rust by Example (en anglais) : de courts exemples exécutables qui comparent closures et fonctions.
- Capturing — Rust by Example (en anglais) : d’autres exemples de closures qui lisent, modifient ou prennent possession de variables.
- Closure expressions — The Rust Reference (en anglais) : les règles exactes de la syntaxe et de la capture des closures.