Chiusure
Le chiusure (closure) di Rust sono funzioni anonime che è possibile salvare in una variabile o passare come argomenti ad altre funzioni. È possibile creare la chiusura in un punto e poi chiamarla altrove per valutarla in un contesto diverso. A differenza delle funzioni, le chiusure possono catturare valori dallo scope in cui sono definite. Dimostreremo come queste funzionalità di chiusura consentano il riutilizzo del codice e la personalizzazione del comportamento.
Catturare l’Ambiente
Esamineremo innanzitutto come possiamo utilizzare le chiusure per catturare valori dall’ambiente in cui sono definite per un uso successivo. Ecco lo scenario: ogni tanto, la nostra azienda di magliette regala una maglietta esclusiva in edizione limitata a qualcuno nella nostra mailing list come promozione. Gli utenti della mailing list possono facoltativamente aggiungere il loro colore preferito al proprio profilo. Se la persona a cui viene assegnata una maglietta gratuita ha impostato il suo colore preferito, riceverà la maglietta di quel colore. Se la persona non ha specificato un colore preferito, riceverà il colore di cui l’azienda ha attualmente la maggiore disponibilità.
Ci sono molti modi per implementarlo. Per questo esempio, useremo un’enum
chiamata ColoreMaglietta
che ha le varianti Rosso
e Blu
(limitando il
numero di colori disponibili per semplicità). Rappresentiamo l’inventario
dell’azienda con una struct Inventario
che ha un campo denominato
magliette
che contiene un Vec<ColoreMaglietta>
che rappresenta i colori
delle magliette attualmente disponibili in magazzino. Il metodo regalo
definito su Inventario
ottiene la preferenza opzionale per il colore della
maglietta del vincitore della maglietta gratuita e restituisce il colore della
maglietta che la persona riceverà. Questa configurazione è mostrata nel Listato
13-1.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ColoreMaglietta {
Rosso,
Blu,
}
struct Inventario {
magliette: Vec<ColoreMaglietta>,
}
impl Inventario {
fn regalo(&self, preferenze_utente: Option<ColoreMaglietta>) -> ColoreMaglietta {
preferenze_utente.unwrap_or_else(|| self.maggior_stock())
}
fn maggior_stock(&self) -> ColoreMaglietta {
let mut num_rosso = 0;
let mut num_blu = 0;
for colore in &self.magliette {
match colore {
ColoreMaglietta::Rosso => num_rosso += 1,
ColoreMaglietta::Blu => num_blu += 1,
}
}
if num_rosso > num_blu {
ColoreMaglietta::Rosso
} else {
ColoreMaglietta::Blu
}
}
}
fn main() {
let negozio = Inventario {
magliette: vec![ColoreMaglietta::Blu, ColoreMaglietta::Rosso, ColoreMaglietta::Blu],
};
let pref_utente1 = Some(ColoreMaglietta::Rosso);
let regalo1 = negozio.regalo(pref_utente1);
println!(
"L'utente con preferenza {:?} riceve {:?}",
pref_utente1, regalo1
);
let pref_utente2 = None;
let regalo2 = negozio.regalo(pref_utente2);
println!(
"L'utente con preferenza {:?} riceve {:?}",
pref_utente2, regalo2
);
}
Il negozio
definito in main
ha due magliette blu e una rossa rimanenti da
distribuire per questa promozione in edizione limitata. Chiamiamo il metodo
regalo
per un utente con preferenza per una maglietta rossa e un utente senza
alcuna preferenza.
Anche in questo caso, questo codice potrebbe essere implementato in molti modi
e, per concentrarci sulle chiusure, ci siamo attenuti ai concetti che hai già
imparato, ad eccezione del corpo del metodo regalo
che utilizza una chiusura.
Nel metodo regalo
, otteniamo la preferenza dell’utente come parametro di
type Option<ColoreMaglietta>
e chiamiamo il metodo unwrap_or_else
su
preferenza_utente
. Il metodo unwrap_or_else
su
Option<T>
è definito dalla libreria standard.
Accetta un argomento: una chiusura senza argomenti che restituisce un valore T
(lo stesso type memorizzato nella variante Some
di Option<T>
, in questo
caso ColoreMaglietta
). Se Option<T>
è la variante Some
, unwrap_or_else
restituisce il valore presente all’interno di Some
. Se Option<T>
è la
variante None
, unwrap_or_else
chiama la chiusura e restituisce il valore
restituito dalla chiusura.
Specifichiamo l’espressione di chiusura || self.maggior_stock()
come argomento
di unwrap_or_else
. Questa è una chiusura che non accetta parametri (se la
chiusura avesse parametri, questi apparirebbero tra le due barre verticali). Il
corpo della chiusura chiama self.maggior_stock()
. Stiamo definendo la chiusura
qui, e l’implementazione di unwrap_or_else
valuterà la chiusura in seguito, se
il risultato è necessario.
L’esecuzione di questo codice stampa quanto segue:
$ cargo run
Compiling azienda-magliette v0.1.0 (file:///progetti/azienda-magliette)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.07s
Running `target/debug/azienda-magliette`
L'utente con preferenza Some(Rosso) riceve Rosso
L'utente con preferenza None riceve Blu
Un aspetto interessante è che abbiamo passato una chiusura che chiama
self.maggior_stock()
sull’istanza corrente di Inventario
. La libreria
standard non aveva bisogno di sapere nulla sui type Inventario
o
ColoreMaglietta
che abbiamo definito, né sulla logica che vogliamo utilizzare
in questo scenario. La chiusura cattura un reference immutabile all’istanza
self
di Inventario
e lo passa con il codice che specifichiamo al metodo
unwrap_or_else
. Le funzioni, d’altra parte, non sono in grado di catturare il
loro ambiente in questo modo.
Inferenza e Annotazione del Type Delle Chiusure
Esistono ulteriori differenze tra funzioni e chiusure. Le chiusure di solito non
richiedono di annotare i type dei parametri o dei valori di ritorno, come
fanno le funzioni fn
. Le annotazioni del type sono necessarie sulle funzioni
perché i type fanno parte di un’interfaccia esplicita esposta agli utenti.
Definire rigidamente questa interfaccia è importante per garantire che tutti
concordino sui tipi di valori che una funzione utilizza e restituisce. Le
chiusure, d’altra parte, non vengono utilizzate in un’interfaccia esposta come
questa: vengono memorizzate in variabili e utilizzate senza denominarle ed
esporle agli utenti della nostra libreria.
Le chiusure sono in genere brevi e rilevanti solo in un contesto ristretto, piuttosto che in uno scenario arbitrario. In questi contesti limitati, il compilatore può dedurre i type dei parametri e il type restituito, in modo simile a come è in grado di dedurre i type della maggior parte delle variabili (ci sono rari casi in cui il compilatore necessita di annotazioni del type anche per le chiusure).
Come per le variabili, possiamo aggiungere annotazioni del type se vogliamo aumentare l’esplicitezza e la chiarezza, a costo di essere più prolissi del necessario. L’annotazione dei type per una chiusura sarebbe simile alla definizione mostrata nel Listato 13-2. In questo esempio, definiamo una chiusura e la memorizziamo in una variabile, anziché definirla nel punto in cui la passiamo come argomento, come abbiamo fatto nel Listato 13-1.
use std::thread; use std::time::Duration; fn genera_allenamento(intensità: u32, numero_casuale: u32) { let chiusura_lenta = |num: u32| -> u32 { println!("calcolo lentamente..."); thread::sleep(Duration::from_secs(2)); num }; if intensità < 25 { println!("Oggi, fai {} flessioni!", chiusura_lenta(intensità)); println!("Poi, fai {} piegamenti!", chiusura_lenta(intensità)); } else { if numero_casuale == 3 { println!("Oggi fai una pausa! Ricordati di idratarti!"); } else { println!( "Oggi, corri per {} minuti!", chiusura_lenta(intensità) ); } } } fn main() { let simulazione_numero_utente = 10; let simulazione_numero_casuale = 7; genera_allenamento(simulazione_numero_utente, simulazione_numero_casuale); }
Con l’aggiunta delle annotazioni del type, la sintassi delle chiusure appare più simile alla sintassi delle funzioni. Qui, per confronto, definiamo una funzione che aggiunge 1 al suo parametro e una chiusura che ha lo stesso comportamento. Abbiamo aggiunto alcuni spazi per allineare le parti rilevanti. Questo illustra come la sintassi delle chiusure sia simile a quella delle funzioni, fatta eccezione per l’uso delle barre verticali e per la quantità di sintassi che è facoltativa:
fn agg_uno_v1 (x: u32) -> u32 { x + 1 }
let agg_uno_v2 = |x: u32| -> u32 { x + 1 };
let agg_uno_v3 = |x| { x + 1 };
let agg_uno_v4 = |x| x + 1 ;
La prima riga mostra una definizione di funzione e la seconda una definizione di
chiusura completamente annotata. Nella terza riga, rimuoviamo le annotazioni del
type dalla definizione della chiusura. Nella quarta riga, rimuoviamo le
parentesi, che sono facoltative perché il corpo della chiusura ha una sola
espressione. Queste sono tutte definizioni valide che produrranno lo stesso
comportamento quando vengono chiamate. Le righe agg_uno_v3
e agg_uno_v4
richiedono che le chiusure vengano valutate per essere compilabili, poiché i
type verranno dedotti dal loro utilizzo. Questo è simile a let v = Vec::new();
che richiede annotazioni del type o valori di qualche tipo da
inserire in Vec
affinché Rust possa dedurne il type.
Per le definizioni delle chiusure, il compilatore dedurrà un type concreto per
ciascuno dei loro parametri e per il loro valore di ritorno. Ad esempio, il
Listato 13-3 mostra la definizione di una chiusura breve che restituisce
semplicemente il valore ricevuto come parametro. Questa chiusura non è molto
utile, se non per gli scopi di questo esempio. Nota che non abbiamo aggiunto
alcuna annotazione del type alla definizione. Poiché non ci sono annotazioni,
possiamo chiamare la chiusura con qualsiasi type, come abbiamo fatto qui con
String
la prima volta. Se poi proviamo a chiamare esempio_chiusura
con un
intero, otterremo un errore.
fn main() {
let esempio_chiusura = |x| x;
let s = esempio_chiusura(String::from("ciao"));
let n = esempio_chiusura(5);
}
Il compilatore ci dà questo errore:
$ cargo run
Compiling esempio-chiusura v0.1.0 (file:///progetti/esempio_chiusura)
error[E0308]: mismatched types
--> src/main.rs:6:30
|
6 | let n = esempio_chiusura(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:5:30
|
5 | let s = esempio_chiusura(String::from("ciao"));
| ---------------- ^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:3:29
|
3 | let esempio_chiusura = |x| x;
| ^
help: try using a conversion method
|
6 | let n = esempio_chiusura(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `esempio_chiusura` (bin "esempio_chiusura") due to 1 previous error
La prima volta che chiamiamo esempio_chiusura
con il valore String
, il
compilatore deduce che il type di x
e il type di ritorno della chiusura
siano String
. Questi type vengono quindi bloccati nella chiusura in
esempio_chiusura
e si verifica un errore di type quando si tenta nuovamente
di utilizzare un type diverso con la stessa chiusura.
Catturare i Reference o Trasferire la Ownership
Le chiusure possono catturare valori dal loro ambiente in tre modi, che corrispondono direttamente ai tre modi in cui una funzione può accettare un parametro: un prestito immutabile, un prestito mutabile o prendendo la ownership. La chiusura deciderà quale di questi utilizzare in base a ciò che il corpo della funzione fa con i valori catturati.
Nel Listato 13-4, definiamo una chiusura che cattura un reference immutabile
al vettore denominato lista
perché necessita solo di un riferimento immutabile
per stampare il valore.
fn main() { let lista = vec![1, 2, 3]; println!("Prima di definire la chiusura: {lista:?}"); let solo_prestito = || println!("Dalla chiusura: {lista:?}"); println!("Prima di chiamare la chiusura: {lista:?}"); solo_prestito(); println!("Dopo aver chiamato la chiusura: {lista:?}"); }
Questo esempio illustra anche che una variabile può essere associata a una definizione di chiusura, e che possiamo successivamente chiamare la chiusura utilizzando il nome della variabile e le parentesi come se il nome della variabile fosse il nome di una funzione.
Poiché possiamo avere più reference immutabili a lista
contemporaneamente,
lista
è comunque accessibile dal codice prima della definizione della
chiusura, dopo la definizione della chiusura ma prima che la chiusura venga
chiamata, e dopo che la chiusura viene chiamata. Questo codice si compila,
esegue e stampa:
$ cargo run
Compiling esempio-chiusura v0.1.0 (file:///progetti/esempio-chiusura)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.09s
Running `target/debug/esempio-chiusura`
Prima di definire la chiusura: [1, 2, 3]
Prima di chiamare la chiusura: [1, 2, 3]
Dalla chiusura: [1, 2, 3]
Dopo aver chiamato la chiusura: [1, 2, 3]
Successivamente, nel Listato 13-5, modifichiamo il corpo della chiusura in modo
che aggiunga un elemento al vettore list
. La chiusura ora cattura un
reference mutabile.
fn main() { let mut lista = vec![1, 2, 3]; println!("Prima di definire la chiusura: {lista:?}"); let mut prestito_mutabile = || lista.push(7); prestito_mutabile(); println!("Dopo aver chiamato la chiusura: {lista:?}"); }
Questo codice si compila, esegue e stampa:
$ cargo run
Compiling esempio-chiusura v0.1.0 (file:///progetti/esempio-chiusura)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.08s
Running `target/debug/esempio-chiusura`
Prima di definire la chiusura: [1, 2, 3]
Dopo aver chiamato la chiusura: [1, 2, 3, 7]
Nota che non c’è più println!
tra la definizione e la chiamata della chiusura
prestito_mutabile
: quando prestito_mutabile
è definita, cattura un
reference mutabile a lista
. Non usiamo più la chiusura dopo che è stata
chiamata, quindi il prestito mutabile termina. Tra la definizione della chiusura
e la chiamata alla chiusura, non è consentito un prestito immutabile per
stampare perché, quando c’è un prestito mutabile, non sono consentiti altri
prestiti. Prova ad aggiungere println!
per vedere quale messaggio di errore
ottieni!
Se vuoi forzare la chiusura ad assumere la ownership dei valori che usa
nell’ambiente, anche se il corpo della chiusura non ne ha strettamente bisogno,
puoi usare la parola chiave move
prima dell’elenco dei parametri.
Questa tecnica è utile soprattutto quando si passa una chiusura a un nuovo
thread per spostare i dati in modo che siano di proprietà del nuovo thread.
Discuteremo i thread e perché dovreste utilizzarli in dettaglio nel Capitolo
16, quando parleremo di concorrenza, ma per ora, esploriamo brevemente la
creazione di un nuovo thread utilizzando una chiusura che richiede la parola
chiave move
. Il Listato 13-6 mostra il Listato 13-4 modificato per stampare il
vettore in un nuovo thread anziché nel thread principale.
use std::thread; fn main() { let lista = vec![1, 2, 3]; println!("Prima di definire la chiusura: {lista:?}"); thread::spawn(move || println!("Dal thread: {lista:?}")) .join() .unwrap(); }
move
per forzare la chiusura affinché il thread prenda la ownership di lista
Generiamo un nuovo thread, assegnandogli una chiusura da eseguire come
argomento. Il corpo della chiusura stampa la lista. Nel Listato 13-4, la
chiusura catturava solo lista
utilizzando un reference immutabile, perché
questo rappresenta il minimo accesso a lista
necessario per stamparlo. In
questo esempio, anche se il corpo della chiusura richiede ancora solo un
reference immutabile, dobbiamo specificare che lista
debba essere spostato
nella chiusura inserendo la parola chiave move
all’inizio della definizione
della chiusura. Se il thread principale eseguisse più operazioni prima di
chiamare join
sul nuovo thread, il nuovo thread potrebbe terminare prima
del thread principale, oppure il thread principale potrebbe terminare per
primo. Se il thread principale mantenesse la ownership di lista
ma
terminasse prima del nuovo thread e de-allocasse la memoria di lista
, il
reference immutabile nel thread non sarebbe valido. Pertanto, il compilatore
richiede che lista
venga spostato nella chiusura assegnata al nuovo thread,
affinché il reference sia valido. Prova a rimuovere la parola chiave move
o
a utilizzare lista
nel thread principale dopo la definizione della chiusura
per vedere quali errori del compilatore ottieni!
Restituire i Valori Catturati dalle Chiusure
Una volta che una chiusura ha catturato un reference o preso la ownership di un valore nell’ambiente in cui è definita (influenzando quindi cosa, se presente, viene spostato all’interno della chiusura), il codice nel corpo della chiusura definisce cosa succede ai reference o ai valori quando la chiusura viene valutata in seguito (influenzando quindi cosa, se presente, viene spostato fuori dalla chiusura).
Il corpo di una chiusura può eseguire una delle seguenti operazioni: spostare un valore catturato fuori dalla chiusura, mutare il valore catturato, non spostare né mutare il valore, oppure non catturare nulla dall’ambiente fin dall’inizio.
Il modo in cui una chiusura cattura e gestisce i valori dell’ambiente influenza
quali trait implementa la chiusura, e i trait sono il modo in cui funzioni e
struct possono specificare quali tipi di chiusure possono utilizzare. Le
chiusure implementeranno automaticamente uno, due o tutti e tre questi trait
Fn
, in modo additivo, a seconda di come il corpo della chiusura gestisce i
valori:
FnOnce
si applica alle chiusure che possono essere chiamate una sola volta. Tutte le chiusure implementano almeno questo trait perché tutte le chiusure possono essere chiamate. Una chiusura che sposta i valori catturati fuori dal suo corpo implementerà soloFnOnce
e nessuno degli altri trattiFn
perché può essere chiamata una sola volta.FnMut
si applica alle chiusure che non spostano i valori catturati fuori dal loro corpo, ma che potrebbero mutarli. Queste chiusure possono essere chiamate più di una volta.Fn
si applica alle chiusure che non spostano i valori catturati fuori dal loro corpo e che non mutano i valori catturati, così come alle chiusure che non catturano nulla dal loro ambiente. Queste chiusure possono essere chiamate più di una volta senza mutare il loro ambiente, il che è importante in casi come quando una chiusura viene chiamata più volte contemporaneamente.
Diamo un’occhiata alla definizione del metodo unwrap_or_else
su Option<T>
che abbiamo usato nel Listato 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(),
}
}
}
Ricorda che T
è il type generico che rappresenta il type del valore nella
variante Some
di un’Option
. Quel T
è anche il type restituito dalla
funzione unwrap_or_else
: il codice che chiama unwrap_or_else
su
un’Option<String>
, ad esempio, otterrà una String
.
Nota inoltre che la funzione unwrap_or_else
ha il parametro di type generico
aggiuntivo F
. F
è il type del parametro denominato f
, che è la chiusura
che forniamo quando chiamiamo unwrap_or_else
.
Il vincolo di trait specificato sul type generico F
è FnOnce() -> T
, il
che significa che F
deve poter essere chiamato una sola volta, non accettare
argomenti e restituire una T
. L’utilizzo di FnOnce
nel vincolo del trait
esprime il limite che unwrap_or_else
non chiamerà f
più di una volta. Nel
corpo di unwrap_or_else
, possiamo vedere che se Option
è Some
, f
non
verrà chiamata. Se Option
è None
, f
verrà chiamata una volta. Poiché tutte
le chiusure implementano FnOnce
, unwrap_or_else
accetta tutti e tre i tipi
di chiusure ed è il più flessibile possibile.
Nota: se ciò che vogliamo fare non richiede l’acquisizione di un valore dall’ambiente, possiamo usare il nome di una funzione anziché una chiusura quando abbiamo bisogno di qualcosa che implementi uno dei trait
Fn
. Ad esempio, su un valoreOption<Vec<T>>
, potremmo chiamareunwrap_or_else(Vec::new)
per ottenere un nuovo vettore vuoto se il valore èNone
. Il compilatore implementa automaticamente qualsiasi dei traitFn
applicabile per una definizione di funzione.
Ora diamo un’occhiata al metodo della libreria standard sort_by_key
, definito
sulle slice, per vedere in che modo differisce da unwrap_or_else
e perché
sort_by_key
utilizza FnMut
invece di FnOnce
come vincolo del trait. La
chiusura riceve un argomento sotto forma di reference all’elemento corrente
nella slice in esame e restituisce un valore di type K
che può essere
ordinato. Questa funzione è utile quando si desidera ordinare una slice in
base a un particolare attributo di ciascun elemento. Nel Listato 13-7, abbiamo
un elenco di istanze di Rettangolo
e utilizziamo sort_by_key
per ordinarle
in base al loro attributo larghezza
dal più stretto al più largo.
#[derive(Debug)] struct Rettangolo { larghezza: u32, altezza: u32, } fn main() { let mut lista = [ Rettangolo { larghezza: 10, altezza: 1 }, Rettangolo { larghezza: 3, altezza: 5 }, Rettangolo { larghezza: 7, altezza: 12 }, ]; lista.sort_by_key(|r| r.larghezza); println!("{lista:#?}"); }
sort_by_key
per ordinare i rettangoli in base alla larghezzaQuesto codice stampa:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.15s
Running `target/debug/rettangoli`
[
Rettangolo {
larghezza: 3,
altezza: 5,
},
Rettangolo {
larghezza: 7,
altezza: 12,
},
Rettangolo {
larghezza: 10,
altezza: 1,
},
]
Il motivo per cui sort_by_key
è definito per accettare una chiusura FnMut
è
che chiama la chiusura più volte: una volta per ogni elemento nella slice. La
chiusura |r| r.larghezza
non cattura, modifica o sposta nulla dal suo
ambiente, quindi soddisfa i requisiti del vincolo di trait.
Al contrario, il Listato 13-8 mostra un esempio di una chiusura che implementa
solo il trait FnOnce
, perché sposta un valore fuori dall’ambiente. Il
compilatore non ci permette di usare questa chiusura con sort_by_key
.
#[derive(Debug)]
struct Rettangolo {
larghezza: u32,
altezza: u32,
}
fn main() {
let mut lista = [
Rettangolo { larghezza: 10, altezza: 1 },
Rettangolo { larghezza: 3, altezza: 5 },
Rettangolo { larghezza: 7, altezza: 12 },
];
let mut azioni_ordinamento = vec![];
let valore = String::from("chiusura chiamata");
lista.sort_by_key(|r| {
azioni_ordinamento.push(valore);
r.larghezza
});
println!("{lista:#?}");
}
FnOnce
con sort_by_key
Questo è un modo artificioso e contorto (che non funziona) per provare a contare
il numero di volte in cui sort_by_key
chiama la chiusura durante l’ordinamento
di lista
. Questo codice tenta di effettuare questo conteggio inserendo
valore
, una String
dall’ambiente della chiusura, nel vettore
azioni_ordinamento
. La chiusura cattura valore
e quindi sposta valore
fuori dalla chiusura trasferendo la ownership di valore
al vettore
azioni_ordinamento
. Questa chiusura può essere chiamata una sola volta; se
provi a chiamarla una seconda volta non funzionerebbe perché valore
non
sarebbe più nell’ambiente da inserire nuovamente in azioni_ordinamento
!
Pertanto, questa chiusura implementa solo FnOnce
. Quando proviamo a compilare
questo codice, otteniamo questo errore che indica che valore
non può essere
spostato fuori dalla chiusura perché la chiusura deve implementare FnMut
:
$ cargo run
Compiling rettangoli v0.1.0 (file:///progetti/rettangoli)
error[E0507]: cannot move out of `valore`, a captured variable in an `FnMut` closure
--> src/main.rs:18:33
|
15 | let valore = String::from("chiusura chiamata");
| ------ captured outer variable
16 |
17 | lista.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | azioni_ordinamento.push(valore);
| ^^^^^^ move occurs because `valore` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
18 | azioni_ordinamento.push(valore.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rettangoli` (bin "rettangoli") due to 1 previous error
L’errore punta alla riga nel corpo della chiusura che sposta valore
fuori
dall’ambiente. Per risolvere questo problema, dobbiamo modificare il corpo della
chiusura in modo che non sposti valori fuori dall’ambiente. Mantenere un
contatore nell’ambiente e incrementarne il valore nel corpo della chiusura è il
modo più semplice per contare il numero di volte in cui la chiusura viene
chiamata. La chiusura nel Listato 13-9 funziona con sort_by_key
perché cattura
solo un reference mutabile al contatore numero_azioni_ordinamento
e può
quindi essere chiamata più volte.
#[derive(Debug)] struct Rettangolo { larghezza: u32, altezza: u32, } fn main() { let mut lista = [ Rettangolo { larghezza: 10, altezza: 1 }, Rettangolo { larghezza: 3, altezza: 5 }, Rettangolo { larghezza: 7, altezza: 12 }, ]; let mut numero_azioni_ordinamento = 0; lista.sort_by_key(|r| { numero_azioni_ordinamento += 1; r.larghezza }); println!("{lista:#?}, ordinato in {numero_azioni_ordinamento} azioni"); }
FnMut
con sort_by_key
I trait Fn
sono importanti quando si definiscono o si utilizzano funzioni o
type che fanno uso di chiusure. Nella prossima sezione, parleremo degli
iteratori. Molti metodi iteratori accettano argomenti chiusura, quindi tieni a
mente questi dettagli sulle chiusure mentre proseguiamo!