Closures in Rust
Le closures in Rust sono funzioni anonime che possono catturare variabili dall’ambiente circostante. Sono simili alle lambda di altri linguaggi, ma con una differenza fondamentale: il sistema di ownership di Rust si applica anche alle closures, determinando come le variabili vengono catturate. Questo si riflette nei tre trait Fn, FnMut e FnOnce.
Sintassi di base
Una closure si definisce con pipe |parametri| seguiti dal corpo. Per closures brevi, il corpo può essere una singola espressione; per quelle più complesse, si usano le parentesi graffe.
fn main() {
let saluta = |nome| println!("Ciao, {nome}!");
saluta("Marco");
saluta("Anna");
let somma = |a, b| a + b;
println!("3 + 4 = {}", somma(3, 4));
// Closure con corpo multi-riga
let calcola = |x: i32| {
let doppio = x * 2;
let triplo = x * 3;
doppio + triplo
};
println!("Risultato: {}", calcola(5)); // 25
}
Inferenza dei tipi
A differenza delle funzioni, le closures non richiedono annotazioni di tipo esplicite. Rust inferisce i tipi dai parametri e dal valore di ritorno in base all’uso.
fn main() {
let quadrato = |x| x * x;
println!("{}", quadrato(5)); // Rust inferisce che x è i32
// Ma si possono annotare esplicitamente se necessario
let dividi = |a: f64, b: f64| -> f64 { a / b };
println!("{:.2}", dividi(10.0, 3.0));
}
Una volta che il tipo viene inferito per un parametro, non può cambiare. Se chiami una closure con un i32, non puoi poi chiamarla con un f64.
Cattura delle variabili
Le closures catturano le variabili dall’ambiente. Rust sceglie automaticamente il modo meno “invasivo” possibile.
Cattura per riferimento immutabile
Se la closure legge solo la variabile, la cattura per riferimento immutabile (&T).
fn main() {
let messaggio = String::from("Buongiorno");
let stampa = || println!("{messaggio}");
stampa();
stampa();
// messaggio è ancora utilizzabile
println!("Lunghezza: {}", messaggio.len());
}
Cattura per riferimento mutabile
Se la closure modifica la variabile, la cattura per riferimento mutabile (&mut T).
fn main() {
let mut contatore = 0;
let mut incrementa = || {
contatore += 1;
println!("Contatore: {contatore}");
};
incrementa(); // 1
incrementa(); // 2
incrementa(); // 3
// Dopo che la closure non è più in uso, contatore è di nuovo accessibile
println!("Valore finale: {contatore}"); // 3
}
Nota: la closure stessa deve essere dichiarata mut perche modifica il suo stato interno.
Cattura per valore
Se la closure ha bisogno dell’ownership del dato, lo cattura per valore (move implicito).
fn main() {
let nomi = vec!["Alice", "Bob"];
let consuma = || {
let _posseduto = nomi; // move implicito
println!("Nomi consumati");
};
consuma();
// nomi non è più accessibile qui
}
La parola chiave move
Con move, si forza la closure a catturare tutte le variabili per valore, trasferendo l’ownership. Questo è essenziale quando la closure deve vivere più a lungo del contesto in cui è stata creata.
fn crea_saluto(nome: String) -> impl Fn() {
move || {
println!("Ciao, {nome}!");
}
}
fn main() {
let saluta = crea_saluto(String::from("Rust"));
saluta(); // Ciao, Rust!
saluta(); // Ciao, Rust!
}
Senza move, il compilatore segnalerebbe un errore perche nome sarebbe un riferimento a una variabile locale che non esiste piĂą dopo il ritorno della funzione.
move è fondamentale anche con i thread:
use std::thread;
fn main() {
let dati = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Dati dal thread: {:?}", dati);
});
handle.join().unwrap();
}
I trait Fn, FnMut e FnOnce
Rust classifica le closures in base a come catturano e usano le variabili dell’ambiente.
Fn - Cattura per riferimento immutabile
La closure può essere chiamata più volte senza modificare lo stato catturato.
fn chiama_due_volte(f: impl Fn()) {
f();
f();
}
fn main() {
let msg = "Ciao";
chiama_due_volte(|| println!("{msg}"));
}
FnMut - Cattura per riferimento mutabile
La closure modifica le variabili catturate. Può essere chiamata più volte.
fn ripeti_tre_volte(mut f: impl FnMut()) {
f();
f();
f();
}
fn main() {
let mut totale = 0;
ripeti_tre_volte(|| {
totale += 10;
println!("Totale: {totale}");
});
}
FnOnce - Cattura per valore (consumo)
La closure consuma le variabili catturate e può essere chiamata una sola volta.
fn esegui_una_volta(f: impl FnOnce() -> String) {
let risultato = f();
println!("Risultato: {risultato}");
}
fn main() {
let nome = String::from("Mondo");
esegui_una_volta(|| {
format!("Ciao, {nome}!")
// nome viene consumato qui
});
}
La gerarchia è: Fn implica FnMut, che implica FnOnce. Una closure Fn può essere usata dove serve FnOnce.
Closures come parametri di funzione
Le closures sono usatissime come parametri, specialmente con i metodi degli iteratori.
fn filtra_e_mappa(numeri: &[i32], filtro: impl Fn(i32) -> bool, mappa: impl Fn(i32) -> i32) -> Vec<i32> {
numeri.iter()
.copied()
.filter(|&n| filtro(n))
.map(mappa)
.collect()
}
fn main() {
let dati = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let risultato = filtra_e_mappa(
&dati,
|n| n % 2 == 0, // Solo pari
|n| n * n, // Eleva al quadrato
);
println!("{:?}", risultato); // [4, 16, 36, 64, 100]
}
Restituire closures
Per restituire una closure da una funzione, si usa impl Fn(...) o Box<dyn Fn(...)>.
fn crea_moltiplicatore(fattore: i32) -> impl Fn(i32) -> i32 {
move |x| x * fattore
}
fn crea_operazione(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"somma" => Box::new(|a, b| a + b),
"prodotto" => Box::new(|a, b| a * b),
_ => Box::new(|a, b| a - b),
}
}
fn main() {
let doppio = crea_moltiplicatore(2);
let triplo = crea_moltiplicatore(3);
println!("Doppio di 5: {}", doppio(5));
println!("Triplo di 5: {}", triplo(5));
let op = crea_operazione("somma");
println!("3 + 4 = {}", op(3, 4));
}
Conclusione
Le closures sono uno strumento fondamentale in Rust, usate estensivamente con iteratori, thread e callback. Il sistema dei trait Fn, FnMut e FnOnce garantisce la sicurezza della memoria anche nelle funzioni anonime. Ricorda: Rust sceglie automaticamente il modo meno restrittivo di cattura; usa move solo quando devi esplicitamente trasferire l’ownership alla closure.