Les closures
Une closure est une fonction anonyme capable de capturer son environnement. C’est la brique de base des chaînes d’itérateurs et des combinateurs d’Option — map, filter, unwrap_or_else en prennent tous une. La particularité de Rust tient à ce que cette capture doit se concilier avec les règles de propriété, ce qui donne naissance à trois traits distincts.
Syntaxe
Contrairement aux fonctions, les annotations de type sont facultatives : le compilateur les déduit du contexte d’utilisation.
Note
Cette inférence n’a lieu qu’une seule fois. Une closure dont les types ont été déduits d’un premier appel ne peut pas être réutilisée avec d’autres types :
La capture de l’environnement
C’est ce qui distingue une closure d’une simple fonction : elle accède aux variables de la portée englobante.
Le compilateur choisit le mode de capture le moins contraignant possible, dans cet ordre :
- par référence partagée
&T, si la closure se contente de lire ; - par référence mutable
&mut T, si elle modifie ; - par valeur, si elle consomme la variable.
Une closure capturant par référence mutable détient un emprunt exclusif tant qu’elle est vivante : la variable d’origine reste inaccessible pendant ce temps, exactement comme le décrit Ownership et emprunt.
Le mot-clé move
move force la capture par valeur, même lorsqu’une référence suffirait :
C’est indispensable dès que la closure doit survivre à la portée qui l’a créée — typiquement pour un fil d’exécution ou une tâche asynchrone, dont la durée de vie est inconnue au moment de la création.
Warning
move ne rend pas la closure consommatrice pour autant. Il décrit comment l’environnement est capturé, pas ce que la closure fait de ce qu’elle a capturé. Une closure move qui se contente de lire reste appelable plusieurs fois.
Les trois traits
Une closure n’a pas de type nommable : le compilateur en fabrique un, anonyme, différent pour chaque closure écrite. Ce type implémente automatiquement un ou plusieurs des trois traits suivants, selon ce que le corps fait de son environnement.
| Trait | Receveur | Ce que la closure fait | Appels possibles |
|---|---|---|---|
FnOnce | self | Consomme l’environnement capturé | Un seul |
FnMut | &mut self | Modifie l’environnement | Plusieurs |
Fn | &self | Se contente de lire | Plusieurs, y compris en parallèle |
Ces traits sont emboîtés : toute closure Fn est aussi FnMut, et toute FnMut est aussi FnOnce. FnOnce est donc la contrainte la plus permissive à l’écriture, mais la plus restrictive à l’usage.
Choisir la contrainte en paramètre
La règle est de demander le moins possible, afin d’accepter le plus d’appelants :
Un repère pratique : si la fonction n’appelle la closure qu’une fois, FnOnce suffit — c’est ce que fait unwrap_or_else. Si elle l’appelle dans une boucle, il faut FnMut, ce qu’exigent map et filter. Fn ne devient nécessaire que lorsque plusieurs appels doivent pouvoir coexister, notamment entre fils d’exécution.
Retourner une closure
Le type étant inexprimable, deux formes existent :
Le move de la première forme est obligatoire : sans lui, la closure emprunterait facteur, une variable locale détruite au retour de la fonction.
Closures et pointeurs de fonction
Une closure qui ne capture rien peut être convertie en pointeur de fonction fn, et réciproquement toute fonction est acceptée là où une closure est attendue :
Ce passage direct du nom de fonction allège beaucoup de chaînes d’itérateurs — map(str::trim), filter_map(|s| s.parse().ok()), map(String::from). Les constructeurs de variantes en profitent aussi, Some et Ok étant eux-mêmes des fonctions :
Pièges courants
Capturer self par mégarde
Dans une méthode, mentionner un champ dans une closure capture self en entier, et non le seul champ concerné. L’emprunt qui en résulte bloque tout autre accès à l’instance.
Info
Depuis l’édition 2021, la capture disjointe améliore ce comportement : une closure capture désormais les champs individuellement plutôt que la structure entière, ce qui résout automatiquement une partie de ces conflits. La capture explicite reste nécessaire lorsque la closure doit survivre à self.
Confondre Fn et fn
fn avec une minuscule est un type, celui des pointeurs de fonction. Fn avec une majuscule est un trait, employé comme contrainte. Le premier n’accepte aucune capture, le second si.
move et types Copy
move déplace la variable capturée, sauf si son type est Copy — auquel cas elle est copiée et reste utilisable :
C’est une source de confusion fréquente quand on teste le comportement de move sur un entier plutôt que sur une String.
À retenir
- Une closure capture son environnement selon le mode le moins contraignant possible ;
moveforce la capture par valeur. FnOnce,FnMutetFndécrivent ce que la closure fait de ce qu’elle a capturé, du plus permissif au plus restrictif.- En paramètre, exiger la contrainte la plus faible qui convienne.
- Une closure sans capture équivaut à un pointeur de fonction, ce qui permet de passer un nom de fonction directement.
Ressources
- Closures — chapitre du Rust Book
std::ops::Fn— la documentation des trois traits- Closure capture in Rust 2021 — la capture disjointe