Fermetures
Les fermetures en Rust sont des fonctions anonymes qui peuvent être sauvegardées dans une variable ou qui peuvent être passées en argument à d’autres fonctions. Il est possible de créer une fermeture à un endroit du code et ensuite de l’appeler ailleurs pour l’exécuter dans un contexte différent. Contrairement aux fonctions, les fermetures ont la possibilité de capturer les valeurs présentes dans le contexte où elles sont appelées. Nous allons montrer comment les fonctionnalités des fermetures permettent de réutiliser du code et suivre des comportements personnalisés.
Capture de l’environnement
Nous allons commencer par voir comment utiliser les fermetures pour récupérer les valeurs de l’environnement dans lequel elles sont définies pour un usage ultérieur. Voici le scénario : de temps à autre, notre entreprise de t-shirts offre un t-shirt exclusif en édition limitée à une personne de notre liste de diffusion à des fins promotionnelles. Les personnes inscrites sur la liste de diffusion peuvent éventuellement ajouter leur couleur préférée à leur profil. Si la personne choisie pour recevoir un t-shirt en cadeau a indiqué sa couleur préférée, elle recevra un t-shirt de cette couleur. Si la personne n’a pas spécifié de couleur préférée, elle aura la couleur des t-shirts dont l’entreprise possède le plus grand nombre.
Il y a bien des manières d’implémenter ceci. Pour cet exemple, nous allons utiliser un enum appeler CouleurTShirt qui aura les variantes Rouge et Bleu (le nombre de couleurs est limité par souci de simplicité). Nous représentons l’inventaire de l’entreprise par une structure Inventaire qui a un champ TShirts contenant un Vec<CouleurTShirt> représentant les couleurs de t-shirts présents en stock. La méthode cadeau définie sur Inventaire prend la couleur préférée du gagnant de t-shirt, et renvoie la couleur du t-shirt que la personne recevra. L’encart 13-1 montre cette organisation.
#[derive(Debug, PartialEq, Copy, Clone)]
enum CouleurTShirt {
Rouge,
Bleu,
}
struct Inventaire {
tshirts: Vec<CouleurTShirt>,
}
impl Inventaire {
fn cadeau(&self, preference_utilisateur: Option<CouleurTShirt>) -> CouleurTShirt {
preference_utilisateur.unwrap_or_else(|| self.le_plus_en_stock())
}
fn le_plus_en_stock(&self) -> CouleurTShirt {
let mut num_rouge = 0;
let mut num_bleu = 0;
for couleur in &self.tshirts {
match couleur {
CouleurTShirt::Rouge => num_rouge += 1,
CouleurTShirt::Bleu => num_bleu += 1,
}
}
if num_rouge > num_bleu {
CouleurTShirt::Rouge
} else {
CouleurTShirt::Bleu
}
}
}
fn main() {
let magasin = Inventaire {
tshirts: vec![CouleurTShirt::Bleu, CouleurTShirt::Rouge, CouleurTShirt::Bleu],
};
let preference_utilisateur1 = Some(CouleurTShirt::Rouge);
let cadeau1 = magasin.cadeau(preference_utilisateur1);
println!(
"L'utilisateur avec comme préférence {:?} reçoit {:?}",
preference_utilisateur1, cadeau1
);
let preference_utilisateur2 = None;
let cadeau2 = magasin.cadeau(preference_utilisateur2);
println!(
"L'utilisateur avec comme préférence {:?} reçoit {:?}",
preference_utilisateur2, cadeau2
);
}
Le magasin défini dans main a encore deux t-shirts bleus et un t-shirt rouge à distribuer dans le cadre de cette promotion en édition limitée. Nous appelons la méthode cadeau pour un utilisateur qui préfère un t-shirt rouge et un utilisateur sans préférence particulière.
Une fois encore, ce code pourrait être implémenté de bien des manières ; ici pour nous focaliser sur les fermetures, nous nous sommes limités aux concepts que vous avez déjà appris, à l’exception du corps de la méthode cadeau qui utilise une fermeture. Dans la méthode cadeau nous récupérons la préférence de l’utilisateur comme un paramètre de type Option<CouleurTShirt> et nous appelons la méthode unwrap_or_else sur preference_utilisateur. La méthode unwrap_or_else sur Option<T> est définie par la bibliothèque standard library. Elle prend un argument : une fermeture sans aucun argument qui renvoie une valeur T (le même type stocké dans la variante Some de Option<T>, dans ce cas CouleurTShirt). Si Option<T> est de variante Some, unwrap_or_else renvoie la valeur contenue dans Some. Si Option<T> est de variante None, unwrap_or_else appelle la fermeture et renvoie la valeur renvoyée par celle-ci.
Nous spécifions l’expression || self.le_plus_en_stockd() en argument de unwrap_or_else. C’est une fermeture qui ne prend pas de paramètres (si la fermeture avait des paramètres, ils apparaîtraient entre les deux barres verticales). Le corps de la fermeture appelle self.le_plus_en_stockd(). Nous définissons la fermeture là, et l’implémentation de unwrap_or_else évaluera la fermeture plus tard si le résultat est requis.
L’exécution de ce code affichera ceci :
$ 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`
L'utilisateur avec comme préférence Some(Rouge) reçoit Rouge
L'utilisateur avec comme préférence None reçoit Bleu
Il est intéressant de noter ici que nous avons passé une fermeture qui appelle self.le_plus_en_stockd() sur l’instance actuelle de Inventaire. La bibliothèque standard n’a pas besoin de connaître les types Inventaire ou CouleurTShirt que nous avons définis, ni la logique que nous souhaitons utiliser dans ce scénario. La fermeture capture une référence immuable à l’instance self Inventaire et la transmet, avec le code que nous spécifions, à la méthode unwrap_or_else. Les fonctions, en revanche, ne sont pas capables de capturer leur environnement de cette manière.
Déduction et annotation des types de fermeture
Il y a d’autres différences 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 type sont nécessaires pour les fonctions car elles font partie d’une interface explicite exposée à leurs utilisateurs. Définir cette interface de manière rigide est nécessaire pour s’assurer que tout le monde s’accorde sur les types de valeurs qu’une fonction utilise et retourne. Les fermetures, en revanche, ne sont pas utilisées dans une interface exposée de cette façon : elles sont stockées dans des variables et utilisées sans les nommer ni les exposer aux utilisateurs de notre bibliothèque.
Les fermetures sont typiquement brèves et ne sont pertinentes que dans un contexte précis plutôt que pour des cas génériques. Dans ces contextes limités, le compilateur est capable de déduire les types des paramètres et le type de retour, tout comme il est capable de déduire les types de la plupart des variables (il y a de rares cas où le compilateur a aussi besoin d’annotations de type de fermetures).
Comme pour les variables, nous pouvons ajouter des annotations de type si nous voulons rendre explicite et clarifier le code au risque d’être plus verbeux que ce qui est strictement nécessaire. Annoter les types d’une fermeture pourrait ressembler à la définition montrée par l’encart 13-2. Dans cet exemple, nous définissons une fermeture et la stockons dans une variable au lieu de définir la fermeture à l’endroit où on la passe comme argument, comme nous l’avions fait dans l’encart 13-1.
use std::thread;
use std::time::Duration;
fn generer_exercices(intensite: u32, nombre_aleatoire: u32) {
let fermeture_lente = |nombre: u32| -> u32 {
println!("calcul très lent ...");
thread::sleep(Duration::from_secs(2));
nombre
};
if intensite < 25 {
println!("Aujourd'hui, faire {} pompes !", fermeture_lente(intensite));
println!("Ensuite, faire {} abdominaux !", fermeture_lente(intensite));
} else {
if nombre_aleatoire == 3 {
println!("Faites une pause aujourd'hui ! Rappelez-vous de bien vous hydrater !");
} else {
println!(
"Aujourd'hui, courez pendant {} minutes !",
fermeture_lente(intensite)
);
}
}
}
fn main() {
let valeur_utilisateur_simule = 10;
let nombre_aleatoire_simule = 7;
generer_exercices(valeur_utilisateur_simule, nombre_aleatoire_simule);
}
La syntaxe des fermetures et des fonctions semble plus similaire avec les annotations de type. Ici, nous définissons une fonction qui ajoute 1 à son paramètre, et d’une fermeture qui a le même comportement, pour comparaison. Nous avons ajouté des espaces pour aligner les parties pertinentes. Ceci met en évidence la similarité entre la syntaxe des fermetures et celle des fonctions, hormis l’utilisation des barres verticales et certaines syntaxes facultatives :
fn ajouter_un_v1 (x: u32) -> u32 { x + 1 }
let ajouter_un_v2 = |x: u32| -> u32 { x + 1 };
let ajouter_un_v3 = |x| { x + 1 };
let ajouter_un_v4 = |x| x + 1 ;
La première ligne affiche la définition d’une fonction et la deuxième ligne une définition d’une fermeture entièrement annotée. Dans la troisième ligne, nous supprimons les annotations de type de la définition de la fermeture. Dans la quatrième ligne, nous supprimons les accolades qui sont facultatives, parce que le corps de la fermeture n’a qu’une seule expression. Ce sont toutes des définitions valides qui suivront le même comportement lorsqu’on les appellera. L’appel aux fermetures est nécessaire pour que ajouter_un_v3 et ajouter_un_v4 puissent être compilés car les types seront déduits en fonction de leur utilisation. Ceci est similaire à let v = Vec::new(); qui a besoin soit des annotations de type, soit de valeurs d’un certain type à insérer dans Vec pour que Rust puisse deviner le type.
Pour ce qui est des définitions des fermetures, le compilateur déduira un type concret pour chacun de leurs paramètres et pour leur valeur de retour. Par exemple, l’encart 13-8 montre la définition d’une petite fermeture qui renvoie simplement la valeur qu’elle reçoit comme paramètre. Cette fermeture n’est pas très utile sauf pour les besoins de cet exemple. Notez que nous n’avons pas ajouté d’annotation de type à la définition. Parce qu’il n’y a pas d’annotations de type, nous pouvons appeler la fermeture avec n’importe quel type, ce que nous avons fait ici en utilisant une String comme argument la première fois et un u32 la deuxième fois. Si nous tentons ensuite d’appeler fermeture_exemple, nous obtiendrons une erreur :
fn main() {
let fermeture_exemple = |x| x;
let s = fermeture_exemple(String::from("hello"));
let n = fermeture_exemple(5);
}
Le compilateur nous renvoie l’erreur suivante :
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = fermeture_exemple(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 = fermeture_exemple(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 fermeture_exemple = |x| x;
| ^
help: try using a conversion method
|
5 | let n = fermeture_exemple(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 fermeture_exemple avec une String, le compilateur déduit que le type de x et le type de retour de la fermeture sont de type String. Ces types sont ensuite verrouillés dans fermeture_exemple, et nous obtenons une erreur de type si après cela nous essayons d’utiliser un type différent avec la même fermeture.
Capture des références ou transfert de propriété
Les fermetures peuvent capturer les valeurs de leur environnement de trois façons différentes, qui correspondent directement aux trois façons dont une fonction peut prendre un paramètre : emprunter de manière immuable, emprunter de manière mutable, et prendre possession. La fermeture décidera laquelle de ces manières elle utilisera, en fonction de ce que le corps de la fonction fait avec les valeurs capturées.
Dans l’encart 13-4, nous définissons une fermeture qui capture une référence immuable vers le vecteur liste parce qu’il a seulement besoin d’une référence immuable pour afficher la valeur.
fn main() {
let liste = vec![1, 2, 3];
println!("Avant la définition de la fermeture : {liste:?}");
let emprunte_seulement = || println!("Depuis la fermeture: {liste:?}");
println!("Avant l'appel à la fermeture : {liste:?}");
emprunte_seulement();
println!("Après l'appel à la fermeture : {liste:?}");
}
Cet exemple illustre également le fait qu’une variable peut être liée à une définition de fermeture, et qu’il est possible d’appeler plus tard la fermeture en utilisant le nom de variable avec des parenthèses, comme si le nom de variable était un nom de fonction.
Comme il est possible d’avoir plusieurs références immuables à liste en même temps, liste est toujours accessible depuis le code avant la définition de la fermeture, après la définition de la fermeture mais avant que l’appel à la fermeture, et après l’appel à la fermeture. Ce code se compile, s’exécute et affiche ceci :
$ 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`
Avant la définition de la fermeture : [1, 2, 3]
Avant l'appel à la fermeture : [1, 2, 3]
Depuis la fermeture: [1, 2, 3]
Après l'appel à la fermeture : [1, 2, 3]
Ensuite, dans l’encart 13-5, nous changeons le corps de la fermeture de manière à ce qu’elle ajoute un élément au vecteur liste. La fermeture capture maintenant une référence mutable.
fn main() {
let mut liste = vec![1, 2, 3];
println!("Avant la définition de la fermeture : {liste:?}");
let mut emprunte_de_maniere_mutable = || liste.push(7);
emprunte_de_maniere_mutable();
println!("Après l'appel à la fermeture : {liste:?}");
}
Ce code se compile, s’exécute et affiche ceci :
$ 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`
Avant la définition de la fermeture : [1, 2, 3]
Après l'appel à la fermeture : [1, 2, 3, 7]
Notez qu’il n’y a plus de println! entre la définition et l’appel à la fermeture emprunte_de_maniere_mutable : quand emprunte_de_maniere_mutable est définie, il capture une référence mutable à liste. Nous n’utilisons pas une nouvelle fois la fermeture après l’appel à la fermeture, donc l’emprunt mutable se termine. Entre la définition de la fermeture et l’appel à la fermeture, un emprunt immutable pour affichage n’est pas autorisé, car aucun autre emprunt n’est autorisé quand il y a un emprunt mutable. Essayez d’ajouter un println! là pour voir quel message d’erreur vous obtenez !
Si nous voulons forcer la fermeture à prendre possession des valeurs qu’elle utilise dans l’environnement même si le corps de la fermeture n’a pas strictement besoin la possession, nous pouvons utiliser le mot-clé move la liste des paramètres.
Cette technique est très utile lorsque vous passez une fermeture à une nouvelle tâche pour déplacer les données afin qu’elles appartiennent à la nouvelle tâche. Nous traiterons en détail des tâches (threads) et de pourquoi vous voudriez les utiliser dans le chapitre 16 quand nous aborderons la concurrence ; mais pour le moment, voyons brièvement comment créer une nouvelle tâche à l’aide d’une fermeture qui nécessite le mot-clé move. L’encart 13-6 montre le contenu de l’encart 13-4 modifié pour afficher le vecteur dans une nouvelle tâche plutôt que dans la tâche principale.
use std::thread;
fn main() {
let liste = vec![1, 2, 3];
println!("Avant la définition de la fermeture : {liste:?}");
thread::spawn(move || println!("Depuis la tâche : {liste:?}"))
.join()
.unwrap();
}
move to force the closure for the thread to take ownership of listNous créons une nouvelle tâche en lui passant comme argument une fermeture à exécuter. Le corps de la fermeture affiche la liste. Dans l’encart 13-4, la fermeture n’a capturé liste qu’à l’aide d’une référence immuable, car c’est le minimum d’accès à liste nécessaire pour l’afficher. Dans cet exemple, bien que le corps de la fermeture n’a besoin que d’une référence immuable, nous devons préciser que liste doit être déplacée dans la fermeture en mettant le mot-clé move au début de la définition de la fermeture. Si la tâche (thread) principale effectuait plus d’opérations avant d’appeler join sur la nouvelle tâche, cette nouvelle tâche pourrait se terminer avant le reste de la tâche principale, ou la tâche principale pourrait se terminer en premier. Si la tâche principale conservait la propriété de liste mais se terminait avant la nouvelle tâche et libérait liste, la référence immuable dans la tâche serait invalide. Par conséquent, le compilateur exige que liste soit déplacée dans la fermeture donnée à la nouvelle tâche, de manière à ce que la référence soit valide. Essayez de supprimer le mot-clé move ou d’utiliser liste dans la tâche principale après la définition de la fermeture pour voir quelles erreurs de compilation vous obtenez !
Déplacement de valeurs capturées en-dehors des fermetures
À partir du moment où une fermeture a capturé une référence ou qu’elle a pris possession d’une valeur provenant de l’environnement où la fermeture est définie (ce qui influence donc ce qui est éventuellement transféré dans la fermeture), le code du corps de la fermeture définit ce qui advient aux références ou aux valeurs quand la fermeture est ultérieurement évaluée (ce qui influence donc ce qui est éventuellement transféré hors de la fermeture).
Un corps de fermeture peut effectuer une des opérations suivantes : déplacer la valeur capturée hors de la fermeture, modifier la valeur capturée, ne déplacer ni ne modifier la valeur, ou ne rien capturer de l’environnement dès le départ.
La manière dont une fermeture capture et traite les valeurs à partir de l’environnement affecte quels traits la fermeture implémente, et ces traits permettent aux fonctions et aux structures de spécifier les sortes de fermetures qu’elles peuvent utiliser. Les fermetures peuvent automatiquement implémenter une, deux ou tous les trois traits Fn, de manière cumulative, selon la façon dont le corps de la fermeture traite les valeurs :
FnOnces’applique aux fermetures qui peuvent être appelées une fois. Toutes les fermetures implémentent au moins ce trait car toutes les fermetures peuvent être appelées. Une fermeture qui déplace les valeurs capturées hors de son corps implémentera uniquementFnOnceet aucun des autres traitsFnparce qu’elle ne peut être appelée qu’une fois.FnMuts’applique aux fermetures qui ne déplacent pas les valeurs capturées en-dehors de leur corps mais pourraient modifier les valeurs capturées. Ces fermetures peuvent être appelées plus d’une fois.Fns’applique aux fermetures qui ne déplacent pas les valeurs capturées hors de leur corps et ne modifient pas les valeurs capturées, ainsi que les fermetures qui ne capturent rien depuis leur environnement. Ces fermetures peuvent être appelées plus d’une fois sans changer leur environnement, ce qui est important dans des cas comme des appels multiples concurrents à une fermeture.
Voyons la définition de la méthode unwrap_or_else sur Option<T> que nous avons utilisée 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(),
}
}
}
Souvenez-vous que T est le type générique représentant le type de la valeur dans la variante Some d’une Option. Ce type T est aussi le type de retour de la fonction unwrap_or_else : le code qui appelle unwrap_or_else sur une Option<String> par exemple, obtiendra une chaîne String.
Next, notice that the unwrap_or_else function has the additional generic type parameter F. The F type is the type of the parameter named f, which is the closure we provide when calling unwrap_or_else.
Le trait lié spécifié sur le type générique F est FnOnce() -> T, ce qui signifie que F doit pouvoir être appelé une fois, ne pas prendre d’arguments, et renvoyer un T. Utiliser FnOnce dans un trait lié 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 implémentent FnOnce, unwrap_or_else accepte les trois sortes de fermetures et est aussi flexible que possible.
Note : si ce que nous voulons faire ne nécessite pas de capturer une valeur depuis l’environnement, nous pouvons utiliser le nom d’une fonction plutôt que d’une fermeture là où nous avons besoin de quelque chose qui implémente un des traits Fn. Par exemple, sur une valeur Option<Vec<T>>, nous pourrions appeler unwrap_or_else(Vec::new) pour obtenir un nouveau vecteur vide si la valeur est None. Le compilateur implémente automatiquement le trait Fn applicable à une définition de fonction.
Voyons maintenant la méthode sort_by_key de la bibliothèque standard, 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 le trait lié. La fermeture reçoit un argument sous la forme d’une référence à l’élément courant de la slice concernée, et elle renvoie une valeur de type K qui peut être ordonnée. Cette fonction s’avère utile quand vous voulez trier une slice en fonction d’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 trier par leur attribut width, de la plus petite à la plus grande.
#[derive(Debug)]
struct Rectangle {
largeur: u32,
hauteur: u32,
}
fn main() {
let mut liste = [
Rectangle { largeur: 10, hauteur: 1 },
Rectangle { largeur: 3, hauteur: 5 },
Rectangle { largeur: 7, hauteur: 12 },
];
liste.sort_by_key(|r| r.largeur);
println!("{liste:#?}");
}
sort_by_key to order rectangles by widthCe code va afficher :
$ 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 {
largeur: 3,
hauteur: 5,
},
Rectangle {
largeur: 7,
hauteur: 12,
},
Rectangle {
largeur: 10,
hauteur: 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 dans la slice. La fermeture |r| r.largeur ne capture ni ne modifie ni ne déplace quoi que ce soit depuis son environnement, elle remplit donc les exigences du trait lié.
Pour comparaison, l’encart 13-8 montre un exemple d’une fermeture qui implémente seulement le trait FnOnce, car elle déplace une valeur en-dehors de son environnement. Le compilateur ne nous permettra pas d’utiliser cette fermeture avec sort_by_key.
#[derive(Debug)]
struct Rectangle {
largeur: u32,
hauteur: u32,
}
fn main() {
let mut liste = [
Rectangle { largeur: 10, hauteur: 1 },
Rectangle { largeur: 3, hauteur: 5 },
Rectangle { largeur: 7, hauteur: 12 },
];
let mut operations_tri = vec![];
let valeur = String::from("fermeture appelée");
liste.sort_by_key(|r| {
operations_tri.push(valeur);
r.largeur
});
println!("{liste:#?}");
}
FnOnce closure with sort_by_keyIl s’agit là d’une manière peu naturelle et alambiquée (et qui ne fonctionne pas) visant à compter le nombre de fois où sort_by_key appelle la fermeture lors du tri de liste. Ce code tente de faire cela en ajoutant valeur — une chaîne String de l’environnement de la fermeture — dans le vecteur sort_operations. La fermeture capture valeur et ensuite déplace valeur hors de la fermeture en transférant la possession de valeur au vecteur sort_operations. Cette fermeture ne peut être appelée qu’une seule fois ; une tentative de l’appeler une seconde fois ne fonctionnerait pas, car valeur ne serait plus dans l’environnement pour être à nouveau ajoutée dans sort_operations ! Par conséquent, cette fermeture n’implémente que FnOnce. Quand nous tentons de compiler ce code, nous obtenons cette erreur comme quoi valeur ne peut pas être déplacée en-dehors de la fermeture, car cette dernière doit implémenter FnMut :
$ 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 valeur = String::from("fermeture appelée");
| ------ -------------------------------- move occurs because `valeur` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | liste.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | operations_tri.push(valeur);
| ^^^^^^ `valeur` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | operations_tri.push(valeur.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 du corps de la fermeture qui déplace valeur hors de l’environnement. Pour résoudre ce problème, nous devons changer le corps de la fermeture de manière à ce qu’elle ne déplace plus de valeurs hors de l’environnement. Garder un compteur dans l’environnement et incrémenter sa valeur dans le corps de la fermeture est une manière bien plus simple de compter le nombre de fois où la fermeture est appelée. La fermeture dans l’encart 13-9 fonctionne avec sort_by_key car elle ne peut capturer qu’une référence mutable au compteur nb_operations_tri et peut dès lors être appelée plus d’une fois.
#[derive(Debug)]
struct Rectangle {
largeur: u32,
hauteur: u32,
}
fn main() {
let mut liste = [
Rectangle { largeur: 10, hauteur: 1 },
Rectangle { largeur: 3, hauteur: 5 },
Rectangle { largeur: 7, hauteur: 12 },
];
let mut nb_operations_tri = 0;
liste.sort_by_key(|r| {
nb_operations_tri += 1;
r.largeur
});
println!("{liste:#?}, triée en {nb_operations_tri} opérations");
}
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 font appel à les fermetures. Dans la prochaine section, nous aborderons les itérateurs. De nombreuses méthodes d’itérateurs prennent des fermetures en arguments, donc gardez bien en tête ces détails concernant les fermetures à mesure que nous progressons !