Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: src/main.rs
#[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
    );
}
Listato 13-1: Situazione di un’azienda di magliette che deve fare un regalo

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.

File: src/main.rs
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);
}
Listato 13-2: Aggiunta di annotazioni facoltative dei type di parametro e valore di ritorno nella chiusura

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.

File: src/main.rs
fn main() {
    let esempio_chiusura = |x| x;

    let s = esempio_chiusura(String::from("ciao"));
    let n = esempio_chiusura(5);
}
Listato 13-3: Tentativo di chiamare una chiusura i cui type sono inferiti con due type diversi

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.

File: src/main.rs
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:?}");
}
Listato 13-4: Definizione e chiamata di una chiusura che cattura un reference immutabile

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.

File: src/main.rs
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:?}");
}
Listato 13-5: Definizione e chiamata di una chiusura che cattura un reference mutabile

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.

File: src/main.rs
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();
}
Listato 13-6: Utilizzo di 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à solo FnOnce e nessuno degli altri tratti Fn 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 valore Option<Vec<T>>, potremmo chiamare unwrap_or_else(Vec::new) per ottenere un nuovo vettore vuoto se il valore è None. Il compilatore implementa automaticamente qualsiasi dei trait Fn 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.

File: src/main.rs
#[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:#?}");
}
Listato 13-7: Utilizzo di sort_by_key per ordinare i rettangoli in base alla larghezza

Questo 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.

File: src/main.rs
#[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:#?}");
}
Listato 13-8: Tentativo di usare una chiusura 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.

File: src/main.rs
#[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");
}
Listato 13-9: È consentito l’utilizzo di una chiusura 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!