Les fonctions et fermetures avancées
Dans cette section, nous allons explorer quelques fonctionnalités avancées liées aux fonctions et aux fermetures, y compris les pointeurs de fonctions et la capacité de retourner des fermetures.
Pointeurs de fonctions
Nous avons déjà vu comment envoyer des fermetures dans des fonctions ; mais vous
pouvez aussi envoyer des fonctions classiques dans d'autres fonctions ! Cette
technique est utile lorsque vous souhaitez envoyer une fonction que vous avez
déjà définie plutôt que de définir une nouvelle fermeture. Vous pouvez faire
ceci avec des pointeurs de fonctions, qui vous permettent d'utiliser des
fonctions en argument d'autres fonctions. Les fonctions nécessitent le type fn
(avec un f minuscule), à ne pas confondre avec le trait de fermeture Fn
. Le
type fn
s'appelle un pointeur de fonction. La syntaxe pour indiquer qu'un
paramètre est un pointeur de fonction ressemble à celle des fermetures, comme
vous pouvez le voir dans l'encart 19-27.
Fichier : src/main.rs
fn ajouter_un(x: i32) -> i32 { x + 1 } fn le_faire_deux_fois(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let reponse = le_faire_deux_fois(ajouter_un, 5); println!("La réponse est : {}", reponse); }
Ce code affiche La réponse est : 12
. Nous avons précisé que le paramètre f
dans le_faire_deux_fois
est une fn
qui prend en argument un paramètre du
type i32
et retourne un i32
. Nous pouvons ensuite appeler f
dans le corps
de le_faire_deux_fois
. Dans main
, nous pouvons envoyer le nom de la fonction
ajouter_un
dans le premier argument de le_faire_deux_fois
.
Contrairement aux fermetures, fn
est un type plutôt qu'un trait, donc nous
indiquons fn
directement comme type de paramètre plutôt que de déclarer un
paramètre de type générique avec un des traits Fn
comme trait lié.
Les pointeurs de fonctions implémentent simultanément les trois traits de fermeture
(Fn
, FnMut
et FnOnce
) afin que vous puissiez toujours envoyer un
pointeur de fonction en argument d'une fonction qui attendait une fermeture. Il
vaut mieux écrire des fonctions qui utilisent un type générique et un des traits
de fermeture afin que vos fonctions puissent accepter soit des fonctions, soit
des fermetures.
Une situation dans laquelle vous ne voudrez accepter que des fn
et pas
des fermetures, est lorsque vous vous interfacez avec du code externe qui n'a
pas de fermetures : les fonctions C peuvent accepter des fonctions en argument,
mais le C n'a pas fermetures.
Comme exemple d'une situation dans laquelle vous pouvez utiliser soit une
fermeture définie directement ou le nom d'une fonction, prenons l'utilisation
de map
. Pour utiliser la fonction map
pour transformer un vecteur de
nombres en vecteur de chaînes de caractères, nous pouvons utiliser une
fermeture, comme ceci :
fn main() { let liste_de_nombres = vec![1, 2, 3]; let liste_de_chaines: Vec<String> = liste_de_nombres.iter().map(|i| i.to_string()).collect(); }
Ou alors nous pouvons utiliser le nom d'une fonction en argument de map
plutôt
qu'une fermeture, comme ceci :
fn main() { let liste_de_nombres = vec![1, 2, 3]; let liste_de_chaines: Vec<String> = liste_de_nombres.iter().map(ToString::to_string).collect(); }
Notez que nous devons utiliser la syntaxe complète que nous avons vue
précédemment dans la section précédente car il
existe plusieurs fonctions disponibles qui s'appellent to_string
. Ici, nous
utilisons la fonction to_string
définie dans le trait ToString
que la
bibliothèque standard a implémenté sur chaque type qui implémente Display
.
Rappelez-vous qu'à la section “Les valeurs d'énumérations” du chapitre 6, nous apprenions que le nom de chaque variante d'énumération que nous déclarons devient aussi une fonction d'initialisation. Nous pouvons utiliser ces fonctions d'initialisation en tant que pointeurs de fonctions qui implémentent les traits de fermetures, ce qui signifie que nous pouvons utiliser les fonctions d'initialisation comme paramètre des méthodes qui acceptent des fermetures, comme ceci :
fn main() { enum Statut { Valeur(u32), Stop, } let liste_de_statuts: Vec<Statut> = (0u32..20).map(Statut::Valeur).collect(); }
Nous avons ici créé des instances de Statut::Valeur
en utilisant chacune des
valeurs u32
présentes dans l'intervalle sur laquelle nous appelons map
en
utilisant la fonction d'initialisation de Statut::Valeur
. Certaines personnes
préfèrent ce style, et d'autres préfèrent utiliser des fermetures. Ces deux approches
se compilent et produisent le même code, vous pouvez donc utiliser le style qui
est le plus clair pour vous.
Retourner des fermetures
Les fermetures sont représentées par des traits, ce qui signifie que vous ne
pouvez pas retourner directement des fermetures. Dans la plupart des situations
où vous auriez voulu retourner un trait, vous pouvez utiliser à la place le
type concret qui implémente le trait comme valeur de retour de la fonction.
Mais vous ne pouvez pas faire ceci avec les fermetures car elles n'ont pas de
type concret qu'elles peuvent retourner ; vous n'êtes pas autorisé à utiliser
le pointeur de fonction fn
comme type de retour, par exemple.
Le code suivant essaye de retourner directement une fermeture, mais ne peut pas se compiler :
fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
Voici l'erreur de compilation :
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn retourne_une_fermeture() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error
Une nouvelle fois l'erreur du trait Sized
! Rust ne sait pas combien de
mémoire sera nécessaire pour stocker la fermeture. Nous avons vu une solution à
ce problème précédemment. Nous pouvons utiliser un objet trait :
fn retourne_une_fermeture() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
Ce code va se compiler à merveille. Pour en savoir plus sur les objets trait, rendez-vous à la section du chapitre 17.
Maintenant, penchons-nous sur les macros !