SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Corso su Closure: Funzioni Anonime che Catturano il Loro Ambiente
Le closure di Rust sono funzioni anonime che puoi salvare in una variabile o passare come argomenti ad altre funzioni. Puoi creare la closure in un punto e poi chiamarla altrove per valutarla in un contesto diverso. A differenza delle funzioni, le closure possono catturare valori dallo scope in cui sono definite. Dimostreremo come queste caratteristiche delle closure consentono il riuso del codice e la personalizzazione del comportamento.
Catturare l’Ambiente con le Closure
Esamineremo innanzitutto come possiamo utilizzare le closure 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 e a tiratura limitata a qualcuno nella nostra mailing list come promozione. Le persone sulla mailing list possono facoltativamente aggiungere il loro colore preferito al loro profilo. Se la persona scelta per una maglietta gratuita ha impostato il suo colore preferito, riceverà quella maglietta di quel colore. Se la persona non ha specificato un colore preferito, riceverà il colore di maglietta di cui l’azienda ha attualmente la maggior quantità.
Ci sono molti modi per implementare questo. Per questo esempio, useremo un enum chiamato ShirtColor che ha le varianti Rosso e Blu (limitando il numero di colori disponibili per semplicità). Rappresentiamo l’inventario dell’azienda con una struttura chiamata Inventory che ha un campo chiamato shirts che contiene un Vec<ShirtColor> che rappresenta i colori delle magliette attualmente in stock. Il metodo giveaway definito su Inventory ottiene la preferenza opzionale di colore di maglietta del vincitore della maglietta gratuita e restituisce il colore della maglietta che la persona riceverà. Questa configurazione è mostrata nel Listato 13-1:
rust
#[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
);
}
La variabile store definita in main ha due magliette blu e una maglietta rossa rimanente da distribuire per questa promozione a tiratura limitata. Chiamiamo il metodo giveaway per un utente con una preferenza per una maglietta rossa e un utente senza alcuna preferenza.
Ancora una volta, questo codice potrebbe essere implementato in molti modi e qui, per concentrarsi sulle closure, ci siamo attenuti a concetti che hai già imparato eccetto per il corpo del metodo giveaway che utilizza una closure. Nel metodo giveaway, otteniamo la preferenza dell’utente come parametro di tipo Option<ShirtColor> e chiamiamo il metodo unwrap_or_else su user_preference. Il metodo unwrap_or_else su Option<T> è definito dalla libreria standard. Prende un solo argomento: una closure senza argomenti che restituisce un valore T (lo stesso tipo memorizzato nella variante Some di Option<T>, in questo caso ShirtColor). Se l’Option<T> è la variante Some, unwrap_or_else restituisce il valore dall’interno di Some. Se l’Option<T> è la variante None, unwrap_or_else chiama la closure e restituisce il valore restituito dalla closure.
Specifichiamo l’espressione di chiusura || self.most_stocked() come argomento di unwrap_or_else. Questa è una closure che non ha parametri propri (se la closure avesse parametri, apparirebbero tra le due barre verticali). Il corpo della closure chiama self.most_stocked(). Stiamo definendo la closure qui e l’implementazione di unwrap_or_else valuterà la closure successivamente se il risultato è necessario.
Eseguendo questo codice viene stampato:
bash
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished dev [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 aspetto interessante qui è che abbiamo passato una closure che chiama self.most_stocked() sull’istanza corrente di Inventory. La libreria standard non ha bisogno di sapere nulla sui tipi Inventory o ShirtColor che abbiamo definito, né sulla logica che vogliamo utilizzare in questo scenario. La closure cattura un riferimento immutabile all’istanza self di Inventory e lo passa con il codice che specificiamo 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 dei Tipi per le Closure
Ci sono altre differenze tra funzioni e closure. Le closure di solito non richiedono di annotare i tipi dei parametri o il valore di ritorno come fanno le funzioni fn. Le annotazioni di tipo sono richieste sulle funzioni perché i tipi fanno parte di un’interfaccia esplicita esposta ai tuoi utenti. Definire questa interfaccia in modo rigido è importante per garantire che tutti concordino su quali tipi di valori una funzione utilizza e restituisce. Le closure, d’altra parte, non vengono utilizzate in un’interfaccia esposta in questo modo: sono memorizzate in variabili e utilizzate senza nominarle ed esporle agli utenti della nostra libreria.
Le closure sono tipicamente brevi e rilevanti solo all’interno di un contesto ristretto piuttosto che in qualsiasi scenario arbitrario. All’interno di questi contesti limitati, il compilatore può inferire i tipi dei parametri e il tipo di ritorno, simile a come è in grado di inferire i tipi della maggior parte delle variabili (ci sono casi rari in cui il compilatore ha bisogno anche di annotazioni di tipo delle closure).
Come per le variabili, possiamo aggiungere annotazioni di tipo se vogliamo aumentare l’esplicità e la chiarezza a discapito di essere più verbosi di quanto sia strettamente necessario. L’annotazione dei tipi per una closure assomiglierebbe alla definizione mostrata nel Listato 13-2. In questo esempio, stiamo definendo una closure e memorizzandola in una variabile anziché definire la closure nel punto in cui la passiamo come argomento come abbiamo fatto nel Listato 13-1.
rust
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
Con le annotazioni di tipo aggiunte, la sintassi delle closure assomiglia di più alla sintassi delle funzioni. Qui definiamo una funzione che aggiunge 1 al suo parametro e una closure che ha lo stesso comportamento, per confronto. Abbiamo aggiunto alcuni spazi per allineare le parti rilevanti. Questo illustra come la sintassi delle closure sia simile alla sintassi delle funzioni tranne per l’uso delle barre verticali e la quantità di sintassi che è opzionale:
rust
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 prima riga mostra una definizione di funzione, e la seconda riga mostra una definizione di closure completamente annotata. Nella terza riga, rimuoviamo le annotazioni di tipo dalla definizione della closure. Nella quarta riga, rimuoviamo le parentesi graffe, che sono opzionali perché il corpo della closure ha solo un’espressione. Queste sono tutte definizioni valide che produrranno lo stesso comportamento quando vengono chiamate. Le linee add_one_v3 e add_one_v4 richiedono che le closure vengano valutate per poter compilare perché i tipi saranno dedotti dal loro utilizzo. Questo è simile a let v = Vec::new(); che richiede annotazioni di tipo o valori di qualche tipo da inserire nel Vec affinché Rust possa dedurre il tipo.
Per le definizioni delle closure, il compilatore dedurrà un tipo concreto per ciascuno dei loro parametri e per il valore di ritorno. Ad esempio, il Listato 13-3 mostra la definizione di una closure breve che restituisce semplicemente il valore che riceve come parametro. Questa closure non è molto utile tranne che per gli scopi di questo esempio. Nota che non abbiamo aggiunto alcuna annotazione di tipo alla definizione. Poiché non ci sono annotazioni di tipo, possiamo chiamare la closure con qualsiasi tipo, che abbiamo fatto qui con String la prima volta. Se poi proviamo a chiamare example_closure con un intero, otterremo un errore.
Corso su Closure: Catturare Riferimenti o Trasferire Proprietà
Le closure possono catturare valori dal loro ambiente in tre modi, che corrispondono direttamente ai tre modi in cui una funzione può prendere un parametro: prendendo in prestito in modo immutabile, prendendo in prestito in modo mutabile e prendendo possesso. La closure deciderà quale di questi usare in base a cosa fa il corpo della funzione con i valori catturati.
Nell’Esempio 1, definiamo una closure che cattura un riferimento immutabile al vettore chiamato list perché ha solo bisogno di un riferimento immutabile per stampare il valore.
rust
fn main() {
let list = vec![1, 2, 3];
println!("Prima di definire closure: {:?}", list); let only_borrows = || println!("Dalla closure: {:?}", list);
println!("Prima di chiamare closure: {:?}", list);
only_borrows();
println!("Dopo aver chiamato closure: {:?}", list);
}
Nell’Esempio 2, modifichiamo il corpo della closure in modo che aggiunga un elemento al vettore list. La closure ora cattura un riferimento mutabile.
rust
fn main() {
let mut list = vec![1, 2, 3];
println!("Prima di definire closure: {:?}", list); let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("Dopo aver chiamato closure: {:?}", list);
}
Se vuoi forzare la closure a prendere la proprietà dei valori che usa nell’ambiente anche se il corpo della closure non ha strettamente bisogno della proprietà, puoi utilizzare la parola chiave move prima dell’elenco dei parametri.
Nell’Esempio 3, modificato dall’Esempio 1, usiamo la parola chiave move per forzare la closure a prendere la proprietà di list.
rust
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Prima di definire closure: {:?}", list);
thread::spawn(move || println!("Dall'altro thread: {:?}", list))
.join()
.unwrap();
}
Questo corso ha lo scopo di illustrare l’utilizzo delle closure e dei loro comportamenti nel catturare riferimenti o trasferire proprietà dall’ambiente circostante.
Corso su Spostamento dei Valori Catturati dalle Closure e gli Fn Traits
Una volta che una closure ha catturato un riferimento o ha acquisito la proprietà di un valore dall’ambiente in cui è definita la closure (influenzando così cosa, se qualcosa, viene spostato nella closure), il codice nel corpo della closure definisce cosa succede ai riferimenti o valori quando la closure viene valutata in seguito (influenzando quindi cosa, se qualcosa, viene spostato fuori dalla closure). Il corpo di una closure può fare una qualsiasi delle seguenti cose: spostare un valore catturato fuori dalla closure, mutare il valore catturato, né spostare né mutare il valore, o non catturare nulla dall’ambiente inizialmente.
Il modo in cui una closure cattura e gestisce i valori dall’ambiente influisce sui trait che la closure implementa, e i trait sono il modo in cui le funzioni e le strutture possono specificare quali tipi di closure possono utilizzare. Le closure implementeranno automaticamente uno, due o tutti e tre di questi trait Fn, in modo additivo, a seconda di come il corpo della closure gestisce i valori:
- FnOnce si applica alle closure che possono essere chiamate una sola volta. Tutte le closure implementano almeno questo trait, perché tutte le closure possono essere chiamate. Una closure che sposta i valori catturati dal suo corpo implementerà solo FnOnce e nessuno degli altri trait Fn, perché può essere chiamata solo una volta.
- FnMut si applica alle closure che non spostano valori catturati dal loro corpo, ma che potrebbero mutare i valori catturati. Queste closure possono essere chiamate più di una volta.
- Fn si applica alle closure che non spostano valori catturati dal loro corpo e che non mutano i valori catturati, così come alle closure che non catturano nulla dal loro ambiente. Queste closure possono essere chiamate più di una volta senza mutare il loro ambiente, il che è importante in casi come chiamare una closure più volte contemporaneamente.
Diamo un’occhiata alla definizione del metodo unwrap_or_else su Option<T> che abbiamo usato nel Listato 13-1:
rust
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(),
}
}
}
Ricordiamo che T è il tipo generico che rappresenta il tipo del valore nella variante Some di un’Opzione. Quel tipo T è anche il tipo di ritorno della funzione unwrap_or_else: il codice che chiama unwrap_or_else su un Option<String>, ad esempio, otterrà una Stringa.
Osserviamo ora che la funzione unwrap_or_else ha il parametro di tipo generico aggiuntivo F. Il tipo F è il tipo del parametro chiamato f, che è la closure che forniamo quando chiamiamo unwrap_or_else.
Il vincolo di trait specificato sul tipo generico F è FnOnce() -> T, il che significa che F deve essere in grado di essere chiamato una sola volta, non prendere argomenti e restituire un T. Utilizzando FnOnce nel vincolo del trait esprime il vincolo che unwrap_or_else chiamerà f al massimo una volta. Nel corpo di unwrap_or_else, possiamo vedere che se l’Opzione è Some, f non verrà chiamata. Se l’Opzione è None, f verrà chiamata una volta. Poiché tutte le closure implementano FnOnce, unwrap_or_else accetta il maggior numero possibile di tipi di closure ed è flessibile quanto possibile.
Nota: Anche le funzioni possono implementare tutti e tre i trait Fn. Se ciò che vogliamo fare non richiede di catturare un valore dall’ambiente, possiamo utilizzare il nome di una funzione anziché una closure quando abbiamo bisogno di qualcosa che implementi uno dei trait Fn. Ad esempio, su un valore Option<Vec<T>>, potremmo chiamare unwrap_or_else(Vec::new) per ottenere un nuovo vettore vuoto se il valore è None.
Ora diamo un’occhiata al metodo della libreria standard sort_by_key definito sulle slice, per capire come differisce da unwrap_or_else e perché sort_by_key usa FnMut invece di FnOnce per il vincolo del trait. La closure riceve un argomento sotto forma di un riferimento all’elemento corrente nella slice in considerazione e restituisce un valore di tipo K che può essere ordinato. Questa funzione è utile quando si desidera ordinare una slice per un attributo particolare di ogni elemento. Nel Listato 13-7, abbiamo una lista di istanze Rectangle e usiamo sort_by_key per ordinarle per il loro attributo larghezza da basso ad alto:
rust
#[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);
}
Il motivo per cui sort_by_key è definito per prendere una closure FnMut è che chiama la closure più volte: una volta per ogni elemento nella slice. La closure |r| r.width non cattura, muta o sposta nulla dal suo ambiente, quindi soddisfa i requisiti del vincolo del trait.
In contrasto, il Listato 13-8 mostra un esempio di una closure che implementa solo il trait FnOnce, perché sposta un valore dall’ambiente. Il compilatore non ci permetterà di usare questa closure con sort_by_key:
rust
[Questo codice non compila!]
#[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("by key called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{:#?}", list);
}
Questo è un modo artificioso e contorto (che non funziona) per cercare di contare il numero di volte in cui sort_by_key viene chiamato quando si ordina list. Questo codice cerca di fare questo conteggio inserendo value, una Stringa dall’ambiente della closure, nel vettore sort_operations. La closure cattura value quindi sposta value fuori dalla closure trasferendo la proprietà di value al vettore sort_operations. Questa closure può essere chiamata una volta; provare a chiamarla una seconda volta non funzionerebbe perché value non sarebbe più nell’ambiente da essere inserito in sort_operations di nuovo! Pertanto, questa closure implementa solo FnOnce. Quando cerchiamo di compilare questo codice, otteniamo questo errore che value non può essere spostato fuori dalla closure perché la closure deve implementare FnMut:
rust
$ 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("by key called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
Per ulteriori informazioni su questo errore, prova `rustc --explain E0507`.
error: could not compile `rectangles` due to previous error
L’errore punta alla riga nel corpo della closure che sposta value dall’ambiente. Per risolvere questo problema, dobbiamo cambiare il corpo della closure in modo che non sposti i valori dall’ambiente. Per contare il numero di volte in cui viene chiamato sort_by_key, mantenere un contatore nell’ambiente e incrementare il suo valore nel corpo della closure è un modo più diretto per calcolarlo. La closure nel Listato 13-9 funziona con sort_by_key perché sta solo catturando un riferimento mutabile al contatore num_sort_operations e può quindi essere chiamata più di una volta:
rust
#[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!("{:#?}, ordinato in {num_sort_operations} operazioni", list);
}
I trait Fn sono importanti quando si definiscono o si utilizzano funzioni o tipi che utilizzano le closure. Nella prossima sezione, discuteremo gli iteratori. Molti metodi degli iteratori accettano argomenti di chiusura, quindi tenete presente questi dettagli sulle closure mentre continuiamo!