00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.