Les closures en Rust : magie ou mathématiques strictes ?
\n\nSi vous avez déjà écrit quelque chose comme |x| x + 1 en Rust, vous avez utilisé des closures. Mais dès que vous essayez de passer cette closure à une fonction ou de la stocker dans une structure, le compilateur se met soudain à exiger : « Quel trait ? Fn, FnMut ou FnOnce ? ».
Et là, beaucoup sont perplexes. Surtout si vous venez de JavaScript, Python ou Ruby, où les closures sont « juste des fonctions qui capturent des variables ». En Rust, c'est plus complexe (et plus intéressant).
\n\nAujourd'hui, nous allons comprendre ce qui se passe réellement sous le capot lorsque Rust décide quel trait attribuer à votre closure. Ce n'est pas de la magie – c'est un système de types strict qui vous protège des courses de données et des pointeurs pendants. C'est parti !
\n\nL'essentiel en bref : les trois traits de closure
\n\nEn Rust, il existe trois traits que les closures implémentent :
\n\n- \n
FnOnce– peut être appelée une seule fois. Elle prend possession des variables capturées. \n FnMut– peut être appelée plusieurs fois, mais peut modifier les variables capturées (elle prend&mut). \n Fn– peut être appelée plusieurs fois et ne modifie pas les variables capturées (elle prend&). \n
Chaque closure implémente automatiquement tous les traits appropriés. Autrement dit, si une closure implémente Fn, elle implémente automatiquement aussi FnMut et FnOnce. Mais l'inverse ne fonctionne pas.
Pourquoi ? Parce que Fn est le plus strict, et FnOnce le plus faible. La hiérarchie d'héritage est la suivante :
FnOnce ← FnMut ← Fn\n\nAutrement dit, FnMut: FnOnce, et Fn: FnMut. Retenez cette échelle – elle est essentielle.
Comment Rust décide-t-il quel trait attribuer à une closure ?
\n\nLe compilateur examine comment la closure utilise les variables capturées. Il existe trois modes de capture :
\n\n- \n
- Par référence (
&T) – la closure ne fait que lire la variable. \n - Par référence mutable (
&mut T) – la closure modifie la variable. \n - Par valeur (
T) – la closure prend possession de la variable. \n
Voici comment cela affecte les traits :
\n\n| Mode de capture | Traits implémentés | Exemple |
|---|---|---|
Uniquement &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T (mais pas T) | FnMut + FnOnce | || x += 1 |
T (possession) | Uniquement FnOnce | || drop(x) ou move || x |
Examinons chaque cas avec du code concret.
\n\nExemple 1 : Lecture seule → Fn
\n\nlet x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// On peut l'appeler autant de fois qu'on veut\nread_closure();\nread_closure();\n\n// Et même la passer à une fonction qui exige Fn\nfn call_fn<F: Fn()>(f: F) {\n f();\n}\ncall_fn(read_closure);\n\nIci, la closure se contente de lire x. Elle n'a pas besoin de le modifier ni de s'en approprier. Rust comprend : « OK, c'est sûr – on donne Fn ».
Exemple 2 : Modification → FnMut
\n\nlet mut x = 42;\nlet mut mut_closure = || {\n x += 1;\n println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// On la passe à une fonction qui exige FnMut\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n f();\n f();\n}\ncall_fn_mut(mut_closure);\n\nRemarquez : nous avons déclaré mut_closure avec let mut. Pourquoi ? Parce qu'appeler FnMut nécessite une référence mutable