Les closures
Les fermetures en Rust sont des fonctions anonymes que vous pouvez enregistrer dans une variable ou passer en arguments a d’autres fonctions. Vous pouvez créer la fermeture à un endroit puis l’appeler ailleurs pour l’evaluer dans un contexte différent. Contrairement aux fonctions, les fermetures peuvent capturer des valeurs de la portée dans laquelle elles sont définies. Nous allons montrer comment ces caracteristiques des fermetures permettent la reutilisation du code et la personnalisation du comportement.
Capturer l’environnement
Nous allons d’abord examiner comment nous pouvons utiliser les fermetures pour capturer des valeurs de l’environnement dans lequel elles sont définies pour une utilisation ulterieure. Voici le scenario : de temps en temps, notre entreprise de T-shirts offre un T-shirt exclusif en édition limitee a quelqu’un de notre liste de diffusion comme promotion. Les personnes inscrites sur la liste de diffusion peuvent optionnellement ajouter leur couleur préférée à leur profil. Si la personne choisie pour un T-shirt gratuit a défini sa couleur préférée, elle recoit un T-shirt de cette couleur. Si la personne n’a pas spécifié de couleur préférée, elle recoit la couleur dont l’entreprise à le plus en stock.
Il existe de nombreuses façons d’implémenter cela. Pour cet exemple, nous allons utiliser un enum appelé ShirtColor qui à les variantes Red et Blue (limitant le nombre de couleurs disponibles par souci de simplicite). Nous representons l’inventaire de l’entreprise avec une struct Inventory qui à un champ nomme shirts contenant un Vec<ShirtColor> representant les couleurs de T-shirts actuellement en stock. La methode giveaway définie sur Inventory obtient la preference optionnelle de couleur de T-shirt du gagnant, et retourné la couleur de T-shirt que la personne recevra. Cette configuration est présentée dans l’encart 13-1.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
Le store défini dans main a deux T-shirts bleus et un T-shirt rouge restants a distribuer pour cette promotion en édition limitee. Nous appelons la methode giveaway pour un utilisateur avec une preference pour un T-shirt rouge et un utilisateur sans aucune preference.
Encore une fois, ce code pourrait être implémenté de nombreuses façons, et ici, pour nous concentrer sur les fermetures, nous nous sommes limites aux concepts que vous avez déjà appris, à l’exception du corps de la methode giveaway qui utilise une fermeture. Dans la methode giveaway, nous obtenons la preference de l’utilisateur sous forme de paramètre de type Option<ShirtColor> et appelons la methode unwrap_or_else sur user_preference. La methode [unwrap_or_else sur Option<T>][unwrap-or-else] est définie par la bibliothèque standard. Elle prend un argument : une fermeture sans aucun argument qui retourné une valeur T (le même type stocké dans la variante Some de Option<T>, dans ce cas ShirtColor). Si Option<T> est la variante Some, unwrap_or_else retourné la valeur contenue dans le Some. Si Option<T> est la variante None, unwrap_or_else appelle la fermeture et retourné la valeur renvoyee par la fermeture.
Nous specifions l’expression de fermeture || self.most_stocked() comme argument de unwrap_or_else. C’est une fermeture qui ne prend aucun paramètre (si la fermeture avait des paramètres, ils apparaîtraient entre les deux barres verticales). Le corps de la fermeture appelle self.most_stocked(). Nous définissons la fermeture ici, et l’implémentation de unwrap_or_else evaluera la fermeture plus tard si le résultat est nécessaire.
L’exécution de ce code affiche le résultat suivant : console {{#include ../listings/ch13-functional-features/listing-13-01/output.txt}}
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Un aspect interessant ici est que nous avons passe une fermeture qui appelle self.most_stocked() sur l’instance actuelle d’Inventory. La bibliothèque standard n’avait pas besoin de connaitre quoi que ce soit sur les types Inventory ou ShirtColor que nous avons définis, ni sur la logique que nous voulons utiliser dans ce scenario. La fermeture capture une référence immuable vers l’instance self d’Inventory et la transmet avec le code que nous specifions à la methode unwrap_or_else. Les fonctions, en revanche, ne sont pas capables de capturer leur environnement de cette maniere.
Inference et annotation des types de fermetures
Il y a d’autres differences entre les fonctions et les fermetures. Les fermetures ne nécessitent généralement pas d’annoter les types des paramètres ou de la valeur de retour comme le font les fonctions fn. Les annotations de types sont requises sur les fonctions car les types font partie d’une interface explicite exposee a vos utilisateurs. Définir cette interface de maniere rigide est important pour s’assurer que tout le monde est d’accord sur les types de valeurs qu’une fonction utilise et retourné. Les fermetures, en revanche, ne sont pas utilisees dans une interface exposee de cette façon : elles sont stockées dans des variables et utilisees sans être nommees ni exposees aux utilisateurs de notre bibliothèque.
Les fermetures sont généralement courtes et pertinentes uniquement dans un contexte restreint plutot que dans n’importe quel scenario arbitraire. Dans ces contextes limites, le compilateur peut inferer les types des paramètres et le type de retour, de la même maniere qu’il est capable d’inferer les types de la plupart des variables (il y a de rares cas où le compilateur a également besoin d’annotations de types pour les fermetures).
Comme avec les variables, nous pouvons ajouter des annotations de types si nous voulons augmenter l’explicite et la clarte au prix d’être plus verbeux que strictement nécessaire. Annoter les types pour une fermeture ressemblerait à la définition montrée dans l’encart 13-2. Dans cet exemple, nous définissons une fermeture et la stockons dans une variable plutot que de définir la fermeture à l’endroit où nous la passons en argument, comme nous l’avons fait dans l’encart 13-1.
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
Avec les annotations de types ajoutées, la syntaxe des fermetures ressemble davantage à la syntaxe des fonctions. Ici, nous définissons une fonction qui ajouté 1 à son paramètre et une fermeture qui à le même comportement, a titre de comparaison. Nous avons ajouté des espaces pour aligner les parties pertinentes. Cela illustre comment la syntaxe des fermetures est similaire à la syntaxe des fonctions, à l’exception de l’utilisation des barres verticales et de la quantite de syntaxe qui est optionnelle :
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
La première ligne montre une définition de fonction et la deuxième ligne montre une définition de fermeture entierement annotee. A la troisième ligne, nous retirons les annotations de types de la définition de la fermeture. A la quatrieme ligne, nous retirons les accolades, qui sont optionnelles car le corps de la fermeture n’a qu’une seule expression. Ce sont toutes des définitions valides qui produiront le même comportement lorsqu’elles seront appelées. Les lignes add_one_v3 et add_one_v4 nécessitent que les fermetures soient evaluees pour pouvoir compiler car les types seront inferes à partir de leur utilisation. C’est similaire a let v = Vec::new(); qui nécessite soit des annotations de types, soit des valeurs d’un certain type a inserer dans le Vec pour que Rust puisse inferer le type.
Pour les définitions de fermetures, le compilateur inferera un type concret pour chacun de leurs paramètres et pour leur valeur de retour. Par exemple, l’encart 13-3 montre la définition d’une courte fermeture qui retourné simplement la valeur qu’elle recoit en paramètre. Cette fermeture n’est pas très utile sauf dans le cadre de cet exemple. Notez que nous n’avons ajouté aucune annotation de type à la définition. Parce qu’il n’y a pas d’annotations de types, nous pouvons appeler la fermeture avec n’importe quel type, ce que nous avons fait ici avec String la première fois. Si nous essayons ensuite d’appeler example_closure avec un entier, nous obtiendrons une erreur.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
Le compilateur nous donne cette erreur : console {{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
La première fois que nous appelons example_closure avec la valeur String, le compilateur infere que le type de x et le type de retour de la fermeture sont String. Ces types sont alors verrouilles dans la fermeture example_closure, et nous obtenons une erreur de type lorsque nous essayons ensuite d’utiliser un type différent avec la même fermeture.
Capturer des références ou transferer la possession
Les fermetures peuvent capturer des valeurs de leur environnement de trois façons, qui correspondent directement aux trois façons dont une fonction peut prendre un paramètre : emprunter de maniere immuable, emprunter de maniere mutable et prendre la possession. La fermeture decidera laquelle de ces methodes utiliser en fonction de ce que le corps de la fonction fait avec les valeurs capturees.
Dans l’encart 13-4, nous définissons une fermeture qui capture une référence immuable vers le vecteur nomme list car elle n’a besoin que d’une référence immuable pour afficher la valeur.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
Cet exemple illustre également qu’une variable peut être liee à une définition de fermeture, et nous pouvons ensuite appeler la fermeture en utilisant le nom de la variable et des parentheses comme si le nom de la variable était un nom de fonction.
Parce que nous pouvons avoir plusieurs références immuables vers list en même temps, list est toujours accessible depuis le code avant la définition de la fermeture, après la définition de la fermeture mais avant que la fermeture ne soit appelée, et après que la fermeture ait été appelée. Ce code compilé, s’exécute et affiche : console {{#include ../listings/ch13-functional-features/listing-13-04/output.txt}}
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
Ensuite, dans l’encart 13-5, nous modifions le corps de la fermeture pour qu’il ajouté un élément au vecteur list. La fermeture capture maintenant une référence mutable.
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
Ce code compilé, s’exécute et affiche : console {{#include ../listings/ch13-functional-features/listing-13-05/output.txt}}
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Notez qu’il n’y a plus de println! entre la définition et l’appel de la fermeture borrows_mutably : quand borrows_mutably est définie, elle capture une référence mutable vers list. Nous n’utilisons plus la fermeture après son appel, donc l’emprunt mutable se terminé. Entre la définition de la fermeture et l’appel de la fermeture, un emprunt immuable pour afficher n’est pas autorise, car aucun autre emprunt n’est permis lorsqu’il y à un emprunt mutable. Essayez d’ajouter un println! à cet endroit pour voir quel message d’erreur vous obtenez !
Si vous voulez forcer la fermeture a prendre la possession des valeurs qu’elle utilise dans l’environnement même si le corps de la fermeture n’a pas strictement besoin de la possession, vous pouvez utiliser le mot-clé move avant la liste de paramètres.
Cette technique est surtout utile lorsque vous passez une fermeture à un nouveau thread pour deplacer les données afin qu’elles soient possedees par le nouveau thread. Nous discuterons des threads et de pourquoi vous voudriez les utiliser en détail dans le chapitre 16 lorsque nous parlerons de la concurrence, mais pour l’instant, explorons brievement la creation d’un nouveau thread en utilisant une fermeture qui nécessite le mot-clé move. L’encart 13-6 montre l’encart 13-4 modifié pour afficher le vecteur dans un nouveau thread plutot que dans le thread principal.
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
move to force the closure for the thread to take ownership of listNous créons un nouveau thread en lui donnant une fermeture a exécuter comme argument. Le corps de la fermeture affiche la liste. Dans l’encart 13-4, la fermeture ne capturait list qu’avec une référence immuable car c’est le minimum d’accès a list nécessaire pour l’afficher. Dans cet exemple, même si le corps de la fermeture n’a toujours besoin que d’une référence immuable, nous devons spécifier que list doit être deplacee dans la fermeture en mettant le mot-clé move au début de la définition de la fermeture. Si le thread principal effectuait d’autres opérations avant d’appeler join sur le nouveau thread, le nouveau thread pourrait se terminer avant que le reste du thread principal ne se terminé, ou le thread principal pourrait se terminer en premier. Si le thread principal conservait la possession de list mais se terminait avant le nouveau thread et liberait list, la référence immuable dans le thread serait invalide. Par consequent, le compilateur exige que list soit deplacee dans la fermeture donnée au nouveau thread pour que la référence soit valide. Essayez de retirer le mot-clé move ou d’utiliser list dans le thread principal après que la fermeture ait été définie pour voir quelles erreurs du compilateur vous obtenez !
Deplacer les valeurs capturees hors des fermetures
Une fois qu’une fermeture a capture une référence ou pris la possession d’une valeur de l’environnement ou la fermeture est définie (affectant ainsi ce qui, le cas echeant, est deplace dans la fermeture), le code dans le corps de la fermeture définit ce qui arrive aux références ou aux valeurs lorsque la fermeture est evaluee plus tard (affectant ainsi ce qui, le cas echeant, est deplace hors de la fermeture).
Le corps d’une fermeture peut faire l’une des choses suivantes : deplacer une valeur capturee hors de la fermeture, muter la valeur capturee, ne ni deplacer ni muter la valeur, ou ne rien capturer de l’environnement au depart.
La façon dont une fermeture capture et gère les valeurs de l’environnement affecte les traits que la fermeture implémenté, et les traits sont la maniere dont les fonctions et les structs peuvent spécifier quels types de fermetures elles peuvent utiliser. Les fermetures implementeront automatiquement un, deux ou les trois traits Fn, de maniere additive, selon la façon dont le corps de la fermeture gère les valeurs :
FnOnces’applique aux fermetures qui peuvent être appelées une seule fois. Toutes les fermetures implementent au moins ce trait car toutes les fermetures peuvent être appelées. Une fermeture qui deplace les valeurs capturees hors de son corps n’implementera queFnOnceet aucun des autres traitsFncar elle ne peut être appelée qu’une seule fois.FnMuts’applique aux fermetures qui ne deplacent pas les valeurs capturees hors de leur corps mais qui pourraient muter les valeurs capturees. Ces fermetures peuvent être appelées plusieurs fois.Fns’applique aux fermetures qui ne deplacent pas les valeurs capturees hors de leur corps et qui ne mutent pas les valeurs capturees, ainsi qu’aux fermetures qui ne capturent rien de leur environnement. Ces fermetures peuvent être appelées plusieurs fois sans muter leur environnement, ce qui est important dans des cas comme l’appel concurrent d’une fermeture plusieurs fois.
Examinons la définition de la methode unwrap_or_else sur Option<T> que nous avons utilisee dans l’encart 13-1 :
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Rappelez-vous que T est le type générique representant le type de la valeur dans la variante Some d’une Option. Ce type T est également le type de retour de la fonction unwrap_or_else : du code qui appelle unwrap_or_else sur une Option<String>, par exemple, obtiendra une String.
Ensuite, remarquez que la fonction unwrap_or_else à le paramètre de type générique supplementaire F. Le type F est le type du paramètre nomme f, qui est la fermeture que nous fournissons lors de l’appel a unwrap_or_else.
La contrainte de trait spécifiée sur le type générique F est FnOnce() -> T, ce qui signifie que F doit pouvoir être appelé une fois, ne prendre aucun argument et retourner un T. Utiliser FnOnce dans la contrainte de trait exprime la contrainte que unwrap_or_else n’appellera pas f plus d’une fois. Dans le corps de unwrap_or_else, nous pouvons voir que si Option est Some, f ne sera pas appelée. Si Option est None, f sera appelée une fois. Parce que toutes les fermetures implementent FnOnce, unwrap_or_else accepte les trois types de fermetures et est aussi flexible que possible.
Remarque : si ce que nous voulons faire ne nécessite pas de capturer une valeur de l’environnement, nous pouvons utiliser le nom d’une fonction plutot qu’une fermeture la où nous avons besoin de quelque chose qui implémenté l’un des traits
Fn. Par exemple, sur une valeurOption<Vec<T>>, nous pourrions appelerunwrap_or_else(Vec::new)pour obtenir un nouveau vecteur vide si la valeur estNone. Le compilateur implémenté automatiquement celui des traitsFnqui est applicable pour une définition de fonction.
Examinons maintenant la methode de la bibliothèque standard sort_by_key, définie sur les slices, pour voir en quoi elle diffère de unwrap_or_else et pourquoi sort_by_key utilise FnMut au lieu de FnOnce pour la contrainte de trait. La fermeture recoit un argument sous la forme d’une référence vers l’élément actuel de la slice considérée, et retourné une valeur de type K qui peut être ordonnee. Cette fonction est utile lorsque vous voulez trier une slice selon un attribut particulier de chaque élément. Dans l’encart 13-7, nous avons une liste d’instances de Rectangle, et nous utilisons sort_by_key pour les ordonner par leur attribut width du plus petit au plus grand.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
sort_by_key to order rectangles by widthCe code affiche : console {{#include ../listings/ch13-functional-features/listing-13-07/output.txt}}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
La raison pour laquelle sort_by_key est définie pour prendre une fermeture FnMut est qu’elle appelle la fermeture plusieurs fois : une fois pour chaque élément de la slice. La fermeture |r| r.width ne capture, ne mute ni ne deplace rien hors de son environnement, donc elle respecte les exigences de la contrainte de trait.
En revanche, l’encart 13-8 montre un exemple de fermeture qui n’implémenté que le trait FnOnce, car elle deplace une valeur hors de l’environnement. Le compilateur ne nous laissera pas utiliser cette fermeture avec sort_by_key.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
FnOnce closure with sort_by_keyIl s’agit d’une façon artificielle et alambiquee (qui ne fonctionne pas) d’essayer de compter le nombre de fois que sort_by_key appelle la fermeture lors du tri de list. Ce code tente de faire ce comptage en poussant value – une String de l’environnement de la fermeture – dans le vecteur sort_operations. La fermeture capture value puis deplace value hors de la fermeture en transferant la possession de value au vecteur sort_operations. Cette fermeture ne peut être appelée qu’une seule fois ; essayer de l’appeler une deuxième fois ne fonctionnerait pas, car value ne serait plus dans l’environnement pour être poussee dans sort_operations à nouveau ! Par consequent, cette fermeture n’implémenté que FnOnce. Lorsque nous essayons de compiler ce code, nous obtenons cette erreur indiquant que value ne peut pas être deplacee hors de la fermeture car la fermeture doit implémenter FnMut : console {{#include ../listings/ch13-functional-features/listing-13-08/output.txt}}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
L’erreur pointe vers la ligne dans le corps de la fermeture qui deplace value hors de l’environnement. Pour corriger cela, nous devons modifier le corps de la fermeture pour qu’il ne deplace pas de valeurs hors de l’environnement. Garder un compteur dans l’environnement et incrementer sa valeur dans le corps de la fermeture est une façon plus directe de compter le nombre de fois que la fermeture est appelée. La fermeture de l’encart 13-9 fonctionne avec sort_by_key car elle ne capture qu’une référence mutable vers le compteur num_sort_operations et peut donc être appelée plus d’une fois.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
FnMut closure with sort_by_key is allowed.Les traits Fn sont importants lors de la définition ou de l’utilisation de fonctions ou de types qui utilisent des fermetures. Dans la prochaine section, nous aborderons les iterateurs. De nombreuses methodes d’iterateurs prennent des fermetures en arguments, alors gardez ces détails sur les fermetures à l’esprit pendant que nous continuons !