Fonction comme argument en Rust : du débutant au pro
\n\nAvez-vous déjà écrit en Python ou JavaScript et passé facilement une fonction à une autre fonction ? Puis vous êtes passé à Rust et vous êtes heurté à un mur ? Ça vous parle. La question "How do you pass a Rust function as a parameter?" est l'une des plus populaires sur Stack Overflow. Et pour cause : le système de types et de possession de Rust ajoute plusieurs couches de complexité, mais ouvre aussi de nouvelles possibilités.
\n\nDans cet article, nous allons explorer trois approches principales : les pointeurs de fonction (fn), les closures et les traits. Vous apprendrez quand utiliser chaque méthode et cesserez de craindre le style fonctionnel en Rust.
Pourquoi est-ce important ?
\nSavoir passer des fonctions en paramètres n'est pas qu'une astuce académique. C'est la base pour :
\n- \n
- Créer des bibliothèques flexibles et réutilisables (ex : itérateurs, gestionnaires d'événements) ; \n
- Implémenter les patrons "Stratégie" et "Observateur" ; \n
- Écrire un code concis et expressif sans une multitude de if-else. \n
Contrairement aux langages dynamiques, Rust vérifie strictement les types et les durées de vie avec son compilateur. Vous devez donc comprendre clairement ce que vous passez : une simple adresse de fonction, une closure capturant le contexte, ou quelque chose de plus complexe.
\n\nApproche 1 : Pointeur de fonction (Function Pointer) — fn
\nLa méthode la plus simple et directe. Si votre fonction ne capture pas de variables externes (elle est "pure"), vous pouvez utiliser le type fn. Attention : fn est un type, tandis que Fn, FnMut, FnOnce sont des traits. Ne les confondez pas !
fn add_one(x: i32) -> i32 {\n x + 1\n}\n\n// Fonction qui prend une autre fonction en paramètre\nfn apply_twice(f: fn(i32) -> i32, value: i32) -> i32 {\n f(f(value))\n}\n\nfn main() {\n let result = apply_twice(add_one, 5);\n println!("Résultat : {}", result); // 7\n}\n\nQuand l'utiliser ? Quand la fonction est connue à la compilation, ne capture pas de variables et que vous n'avez pas besoin des performances des closures. Les pointeurs de fonction sont une abstraction zero-cost, sans allocation mémoire.
\n\nLimitations de fn
\nLe principal inconvénient : vous ne pouvez pas passer une closure qui capture des variables. Le code suivant ne compilera pas :
\n\nfn main() {\n let y = 10;\n // La closure capture y\n let closure = |x: i32| -> i32 { x + y };\n // Erreur ! expected fn item, found closure\n // let result = apply_twice(closure, 5);\n}\n\nPour cela, il faut utiliser les traits Fn, FnMut, FnOnce.
Approche 2 : Closures et traits Fn, FnMut, FnOnce
\nRust est un langage système, il doit donc savoir ce qui se passe avec la mémoire. Les closures peuvent capturer des variables de trois manières :
\n- \n
- Par référence (&T) — trait
Fn. Peut être appelée autant de fois que voulu, sans modifier les données capturées. \n - Par référence mutable (&mut T) — trait
FnMut. Peut être appelée plusieurs fois, mais les données peuvent être modifiées. \n - Par valeur (T) — trait
FnOnce. Ne peut être appelée qu'une seule fois, après quoi la closure est "consommée". \n
Exemple avec Fn
\nfn apply_twice_fn<F>(f: F, value: i32) -> i32\nwhere\n F: Fn(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let y = 10;\n let closure = |x| x + y; // capture y par référence\n let result = apply_twice_fn(closure, 5);\n println!("Fn : {}", result); // 25\n}\n\nExemple avec FnMut
\nfn apply_twice_mut<F>(mut f: F, value: i32) -> i32\nwhere\n F: FnMut(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let mut counter = 0;\n let mut closure = |x: i32| -> i32 {\n