Les fonctions et les closures avancées
Cette section explore quelques fonctionnalités avancées liées aux fonctions et aux fermetures, notamment les pointeurs de fonction et le retour de fermetures.
Les pointeurs de fonction
Nous avons parlé de comment passer des fermetures aux fonctions ; vous pouvez aussi passer des fonctions normales aux fonctions ! Cette technique est utile lorsque vous voulez passer une fonction que vous avez déjà définie plutôt que de définir une nouvelle fermeture. Les fonctions se convertissent implicitement vers le type fn (avec un f minuscule), à ne pas confondre avec le trait de fermeture Fn. Le type fn est appelé un pointeur de fonction. Passer des fonctions avec des pointeurs de fonction vous permettra d’utiliser des fonctions comme arguments d’autres fonctions.
La syntaxe pour spécifier qu’un paramètre est un pointeur de fonction est similaire à celle des fermetures, comme montré dans l’encart 20-28, où nous avons défini une fonction add_one qui ajouté 1 à son paramètre. La fonction do_twice prend deux paramètres : un pointeur de fonction vers toute fonction qui prend un paramètre i32 et retourné un i32, et une valeur i32. La fonction do_twice appelle la fonction f deux fois, en lui passant la valeur arg, puis additionne les deux résultats des appels de fonction. La fonction main appelle do_twice avec les arguments add_one et 5.
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
fn type to accept a function pointer as an argumentCe code affiche The answer is: 12. Nous spécifions que le paramètre f dans do_twice est un fn qui prend un paramètre de type i32 et retourné un i32. Nous pouvons ensuite appeler f dans le corps de do_twice. Dans main, nous pouvons passer le nom de fonction add_one comme premier argument à do_twice.
Contrairement aux fermetures, fn est un type plutôt qu’un trait, donc nous spécifions fn comme type de paramètre directement plutôt que de déclarer un paramètre de type générique avec l’un des traits Fn comme contrainte de trait.
Les pointeurs de fonction implémentent les trois traits de fermeture (Fn, FnMut et FnOnce), ce qui signifie que vous pouvez toujours passer un pointeur de fonction comme argument d’une fonction qui attend une fermeture. Il est préférable d’écrire des fonctions utilisant un type générique et l’un des traits de fermeture afin que vos fonctions puissent accepter aussi bien des fonctions que des fermetures.
Cela dit, un exemple où vous voudriez n’accepter que fn et pas les fermetures est lors de l’interfaçage avec du code externe qui n’a pas de fermetures : les fonctions C peuvent accepter des fonctions comme arguments, mais C n’a pas de fermetures.
Comme exemple d’un cas où vous pourriez utiliser soit une fermeture définie en ligne soit une fonction nommée, regardons une utilisation de la méthode map fournie par le trait Iterator de la bibliothèque standard. Pour utiliser la méthode map afin de transformer un vecteur de nombres en un vecteur de chaînes de caractères, nous pourrions utiliser une fermeture, comme dans l’encart 20-29.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
map method to convert numbers to stringsOu nous pourrions nommer une fonction comme argument de map au lieu de la fermeture. L’encart 20-30 montre à quoi cela ressemblerait.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
String::to_string function with the map method to convert numbers to stringsNotez que nous devons utiliser la syntaxe complètement qualifiée dont nous avons parlé dans la section « Traits avancés » plus tôt dans ce chapitre. Parce qu’il y à plusieurs fonctions appelées f (une pour Dog et une pour Animal), Rust ne peut pas déterminer à quelle fonction nous nous référons sans la syntaxe complètement qualifiée.
Ici, nous utilisons la fonction to_string définie dans le trait ToString, que la bibliothèque standard a implémenté pour tout type qui implémente Display.
Rappelez-vous dans la section « Les valeurs d’enum » du chapitre 6 que le nom que nous définissons pour chaque variante d’enum devient aussi une fonction d’initialisation pour la variante. Nous pouvons utiliser ces fonctions d’initialisation comme des pointeurs de fonction qui implémentent les traits de fermeture, ce qui signifie que nous pouvons spécifier les fonctions d’initialisation comme arguments pour les méthodes qui prennent des fermetures.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map method to create a Status instance from numbersIci, nous créons des instances de Status::Value en utilisant chaque valeur u32 de l’intervalle sur lequel map est appelé en utilisant la fonction d’initialisation de Status::Value. Certaines personnes préfèrent ce style et d’autres préfèrent utiliser des fermetures. Ils compilent vers le même code, donc utilisez le style qui vous semble le plus clair.
Retourner des fermetures
Les fermetures sont représentées par des traits, ce qui signifie que vous ne pouvez pas retourner des fermetures directement. Dans la plupart des cas où vous voudriez retourner un trait, vous pouvez utiliser à la place le type concret qui implémente le trait comme valeur de retour de la fonction. Cependant, vous ne pouvez généralement pas faire cela avec les fermetures car elles n’ont pas de type concret qui peut être retourné ; vous n’êtes pas autorisé à utiliser le pointeur de fonction fn comme type de retour si la fermeture capture des valeurs de sa portée, par exemple.
À la place, vous utiliserez normalement la syntaxe impl Trait que nous avons apprise au chapitre 10. Vous pouvez retourner n’importe quel type de fonction, en utilisant Fn, FnOnce et FnMut. Par exemple, le code de l’encart 20-32 compilera sans problème.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl Trait syntaxCependant, comme nous l’avons noté dans la section « Inférer et annoter les types de fermeture », le compilateur a tendance à inférer un type concret pour chaque paramètre de fermeture ou type de retour. Cela signifie que vous ne pouvez pas retourner deux fermetures différentes qui implémentent le même trait en tant que types abstraits.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> of closures defined by functions that return impl Fn typesIci nous avons deux fonctions, returns_closure et returns_initialized_closure, qui retournent toutes les deux impl Fn(i32) -> i32. Remarquez que les fermetures qu’elles retournent sont différentes, même si elles implémentent le même type. Si nous essayons de compiler ceci, Rust nous fait savoir que cela ne fonctionnera pas : text {{#include ../listings/ch20-advanced-features/listing-20-33/output.txt}}
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
Le message d’erreur nous dit que chaque fois que nous retournons un impl Trait, Rust crée un type opaque unique, un type dont nous ne pouvons pas voir les détails de ce que Rust construit pour nous, et dont nous ne pouvons pas deviner le type que Rust générera pour l’écrire nous-mêmes. Ainsi, même si ces fonctions retournent des fermetures qui implémentent le même trait, Fn(i32) -> i32, les types opaques que Rust génère pour chacune sont distincts. (C’est similaire à la façon dont Rust produit des types concrets différents pour des blocs async distincts même quand ils ont le même type de sortie, comme nous l’avons vu dans [“Le type Pin et le trait Unpin”][future-types] au chapitre 17.) Nous avons vu une solution à ce problème plusieurs fois maintenant : nous pouvons utiliser un objet trait, comme dans l’encart 20-34.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
Vec<T> of closures defined by functions that return Box<dyn Fn> so that they have the same typeCe code compilera sans problème. Pour en savoir plus sur les objets trait, référez-vous à la section [“Utiliser les objets trait pour abstraire un comportement partagé”][trait-objects] du chapitre 18.
Ensuite, regardons les macros !