Closure in Rust
Una closure è una piccola funzione senza nome che puoi salvare in una variabile e passare in giro. Quello che la rende diversa da una funzione normale è che può «catturare» le variabili dello scope in cui è stata creata.
Sintassi di base di una closure
Sezione intitolata “Sintassi di base di una closure”fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Output:
6Le barre verticali |x: i32| prendono il posto delle parentesi tonde di una funzione normale. Confronta le due versioni fianco a fianco:
fn add_one_fn(x: i32) -> i32 { x + 1 } // una funzione normalelet add_one_closure = |x: i32| x + 1; // la stessa cosa come closureSpesso Rust riesce anche a dedurre i tipi dei parametri e del valore di ritorno, quindi vedrai spesso closure scritte in modo ancora più breve:
let add_one = |x| x + 1;Una closure senza parametri usa due barre vuote: let say_hi = || println!("ciao");, e la chiami con say_hi().
Closure su più righe
Sezione intitolata “Closure su più righe”Se ti serve più di un’espressione, racchiudi il corpo tra graffe, proprio come in una funzione:
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "pari" } else { "dispari" }; format!("{n} è {kind}") };
println!("{}", describe(7));}Output:
7 è dispariformat! funziona come println!, ma invece di stampare il testo te lo restituisce come String. Qui quella String è l’ultima espressione tra le graffe, quindi è quello che la closure restituisce.
Catturare le variabili dello scope circostante
Sezione intitolata “Catturare le variabili dello scope circostante”Ecco la caratteristica che una funzione normale non ha: una closure può usare le variabili del punto in cui è stata definita, senza che tu gliele passi esplicitamente:
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}Output:
9040discount non è un parametro della closure: sta semplicemente nello scope di main, e la closure allunga la mano e la usa direttamente. Una normale fn non può farlo: avrebbe bisogno che discount le venisse passata come argomento.
Closure che modificano le variabili catturate
Sezione intitolata “Closure che modificano le variabili catturate”Una closure può anche modificare una variabile che cattura, purché quella variabile sia mut. C’è una regola in più: anche la closure stessa va salvata in una variabile mut. Chiamarla cambia qualcosa, e Rust vuole che questo si veda nel codice.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("count ora vale {count}"); };
increment(); increment(); increment();
println!("valore finale di count: {count}");}Output:
count ora vale 1count ora vale 2count ora vale 3valore finale di count: 3Se dimentichi il mut su increment, il compilatore ti dice esattamente cosa non va: calling increment requires mutable binding due to mutable borrow of count (chiamare increment richiede un binding mutabile, perché prende count in prestito in modo mutabile).
C’è un dettaglio che vale la pena conoscere. Finché la closure può ancora essere chiamata, ha il diritto di modificare count, quindi nessun altro può toccare count. Se aggiungi println!("{count}"); tra due chiamate a increment(), ottieni error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable: non puoi prendere count in prestito in modo immutabile, perché è già in prestito in modo mutabile. Dopo l’ultima chiamata a increment() la closure ha finito, e count torna tutta tua. Per questo la riga con il valore finale funziona. La storia completa dietro questa regola la trovi nella pagina sul Borrowing.
A volte una closure deve prendere il pieno possesso di quello che cattura, per esempio quando viene passata a un altro thread. Per farlo scrivi move prima delle barre: move || { ... }. Lo vedrai in azione nella pagina sulla Concorrenza.
Dove si usano davvero le closure
Sezione intitolata “Dove si usano davvero le closure”Raramente userai una closure da sola: dove sono davvero utili è nel passare un comportamento personalizzato a un’altra funzione, soprattutto ai metodi degli iteratori che incontrerai nella pagina sugli Iteratori:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Output:
[2, 4, 6, 8, 10]Qui |n| n * 2 è una closure passata direttamente a .map(). Questo schema (una piccola closure che guida un metodo come .map() o .filter()) è il modo in cui le userai tutti i giorni.
Pratica
Sezione intitolata “Pratica”1. Prezzi con le tasse
Sezione intitolata “1. Prezzi con le tasse”Crea una variabile tax_percent uguale a 20. Poi scrivi una closure with_tax che riceve un prezzo e restituisce il prezzo più le tasse, usando la tax_percent catturata. Stampa il risultato per i prezzi 50 e 120. Suggerimento: le tasse sono price * tax_percent / 100.
Mostra la soluzione
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 con le tasse fa {}", with_tax(50)); println!("120 con le tasse fa {}", with_tax(120));}Output:
50 con le tasse fa 60120 con le tasse fa 144tax_percent non viene mai passata. La closure la cattura da main, ed è proprio quello che una normale fn non potrebbe fare.
2. Un generatore di ID
Sezione intitolata “2. Un generatore di ID”Scrivi una closure next_id che restituisce 1 la prima volta che la chiami, 2 la seconda, 3 la terza e così via. Chiamala tre volte e stampa ogni ID. Suggerimento: tieni un contatore mut fuori dalla closure, aggiungigli 1 dentro, e fai del contatore l’ultima espressione della closure, così viene restituito.
Mostra la soluzione
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("il primo utente riceve l'id {}", next_id()); println!("il secondo utente riceve l'id {}", next_id()); println!("il terzo utente riceve l'id {}", next_id());}Output:
il primo utente riceve l'id 1il secondo utente riceve l'id 2il terzo utente riceve l'id 3Sia last_id sia next_id hanno bisogno di mut: la prima perché il suo valore cambia, la seconda perché chiamare la closure modifica ciò che ha catturato.
Riepilogo
Sezione intitolata “Riepilogo”- Una closure è una piccola funzione anonima:
|parametri| espressione. - Spesso i tipi vengono dedotti, quindi le closure sono di solito più brevi della
fnequivalente. - A differenza di una funzione normale, una closure può catturare le variabili dello scope in cui è stata definita.
- Una closure che modifica una variabile catturata va salvata in una variabile
mut, e tiene in prestito quella variabile per tutto il tempo in cui viene usata. - Le closure sono utilissime passate ad altre funzioni, soprattutto ai metodi degli iteratori come
.map().
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quali simboli usa una closure per i parametri, al posto delle parentesi di una funzione?
2. Che cosa può fare una closure che una normale fn non può fare?
3. Secondo questa pagina, dove si usano più spesso le closure nella pratica?
4. Una closure fa count += 1 su una variabile catturata. Che cos'altro ti serve?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Closures — The Rust Book (in inglese): il capitolo ufficiale sulle closure, l’inferenza di tipo e i trait
Fn. - Capturing References or Moving Ownership — The Rust Book (in inglese): i tre modi in cui una closure può catturare una variabile, e quando usare
move. - Closures — Rust by Example (in inglese): brevi esempi eseguibili che confrontano closure e funzioni.
- Capturing — Rust by Example (in inglese): altri esempi di closure che leggono, modificano e prendono possesso delle variabili.
- Closure expressions — The Rust Reference (in inglese): le regole esatte su sintassi e cattura delle closure.