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

Eseguire del Codice Durante la Pulizia con il Trait Drop

Il secondo trait importante per i puntatori intelligenti è Drop, che consente di personalizzare ciò che accade quando un valore sta per uscire dallo scope. È possibile fornire un’implementazione per il trait Drop su qualsiasi type e tale codice può essere utilizzato per rilasciare risorse come file o connessioni di rete.

Stiamo introducendo Drop nel contesto dei puntatori intelligenti perché la funzionalità del trait Drop viene quasi sempre utilizzata quando si implementa un puntatore intelligente. Ad esempio, quando una Box<T> viene eliminata, de-alloca lo spazio nell’heap a cui punta la box.

In alcuni linguaggi, per alcuni type, il programmatore deve richiamare il codice per liberare memoria o risorse ogni volta che termina di utilizzare un’istanza di quei type. Esempi includono handle a file, socket e lock. Se il programmatore dimentica di farlo, al sistema potrebbe riempirsi la memoria ed eventualmente bloccarsi. In Rust, è possibile specificare che un particolare pezzo di codice venga eseguito ogni volta che un valore esce dallo scope, e il compilatore inserirà questo codice automaticamente. Di conseguenza, non è necessario prestare attenzione a inserire codice di de-allocazione ovunque in un programma in cui un’istanza di un particolare type viene rilasciata: non si saranno problemi di memoria!

Si specifica il codice da eseguire quando un valore esce dallo scope implementando il trait Drop. Il trait Drop richiede l’implementazione di un metodo chiamato drop che accetta un reference mutabile a self. Per vedere quando Rust chiama drop, implementiamo per ora drop con istruzioni println!.

Il Listato 15-14 mostra una struct MioSmartPointer la cui unica funzionalità personalizzata è quella di stampare Pulizia MioSmartPointer! quando l’istanza esce dallo scope, per mostrare quando Rust esegue il metodo drop.

File: src/main.rs
struct MioSmartPointer {
    data: String,
}

impl Drop for MioSmartPointer {
    fn drop(&mut self) {
        println!("Pulizia MioSmartPointer con dati `{}`!", self.data);
    }
}

fn main() {
    let c = MioSmartPointer {
        data: String::from("mia roba"),
    };
    let d = MioSmartPointer {
        data: String::from("altra roba"),
    };
    println!("MioSmartPointer creati.");
}
Listato 15-14: Una struct MioSmartPointer che implementa il trait Drop dove inseriremo il nostro codice di de-allocazione

Il trait Drop è incluso nel preludio, quindi non è necessario portarlo in scope. Implementiamo il trait Drop su MioSmartPointer e forniamo un’implementazione per il metodo drop che chiama println!. Il corpo del metodo drop è dove inseriremmo qualsiasi logica che si desidera eseguire quando un’istanza del type esce dallo scope. Stiamo stampando del testo per mostrare visivamente quando Rust chiamerà drop.

In main, creiamo due istanze di MioSmartPointer e poi stampiamo MioSmartPointer creati. Alla fine di main, le nostre istanze di MioSmartPointer usciranno dallo scope e Rust chiamerà il codice che abbiamo inserito nel metodo drop, stampando il nostro messaggio finale. Nota che non è stato necessario chiamare esplicitamente il metodo drop.

Quando eseguiamo questo programma, vedremo il seguente output:

$ cargo run
   Compiling esempio-drop v0.1.0 (file:///progetti/esempio-drop)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.02s
     Running `target/debug/esempio-drop`
MioSmartPointer creati.
Pulizia MioSmartPointer con dati `altra roba`!
Pulizia MioSmartPointer con dati `mia roba`!

Rust ha chiamato automaticamente drop per noi quando le nostre istanze sono uscite dallo scope, eseguendo il codice che abbiamo specificato. Le variabili vengono eliminate nell’ordine inverso alla loro creazione, quindi d è stata eliminata prima di c. Lo scopo di questo esempio è fornire una guida visiva al funzionamento del metodo drop; Di solito, si specifica il codice di de-allocazione che il type deve eseguire anziché un messaggio di stampa.

Purtroppo, non è semplice disabilitare la funzionalità automatica drop. Disabilitare drop di solito non è necessario; lo scopo del trait Drop è che venga gestito automaticamente. Occasionalmente, tuttavia, potrebbe essere necessario rilasciare un valore in anticipo. Un esempio è quando si utilizzano puntatori intelligenti che gestiscono i blocchi: potresti voler forzare il metodo drop per rilasciare il blocco in modo che altro codice nello stesso scope possa acquisire il blocco. Rust non consente di chiamare manualmente il metodo drop del trait Drop; invece, è necessario chiamare la funzione std::mem::drop fornita dalla libreria standard se si desidera forzare l’eliminazione di un valore prima della fine del suo scope.

Provare a chiamare manualmente il metodo drop del trait Drop modificando la funzione main del Listato 15-14, come mostrato nel Listato 15-15, risulterà in un errore del compilatore.

File: src/main.rs
struct MioSmartPointer {
    data: String,
}

impl Drop for MioSmartPointer {
    fn drop(&mut self) {
        println!("Pulizia MioSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = MioSmartPointer {
        data: String::from("alcuni dati"),
    };
    println!("MioSmartPointer creato.");
    c.drop();
    println!("MioSmartPointer pulito prima della fine di main.");
}
Listato 15-15: Tentativo di chiamare manualmente il metodo drop del trait Drop per una de-allocazione anticipata

Quando proviamo a compilare questo codice, otterremo questo errore:

$ cargo run
   Compiling esempio-drop v0.1.0 (file:///progetti/esempio-drop)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   |
help: consider using `drop` function
   |
16 |     drop(c);
   |     +++++ ~

For more information about this error, try `rustc --explain E0040`.
error: could not compile `esempio-drop` (bin "esempio-drop") due to 1 previous error

Questo messaggio di errore indica che non siamo autorizzati a chiamare esplicitamente drop. Il messaggio di errore utilizza il termine destructor, che è il termine di programmazione generale per una funzione che de-alloca un’istanza. Un destructor è analogo a un constructor, che crea un’istanza. La funzione drop in Rust è una forma particolare di distruttore.

Rust non ci permette di chiamare drop esplicitamente perché Rust chiamerebbe comunque automaticamente drop sul valore alla fine di main. Questo causerebbe un errore di tipo double free perché Rust cercherebbe di de-allocare lo stesso valore due volte.

Non possiamo disabilitare l’inserimento automatico di drop quando un valore esce dallo scope, e non possiamo chiamare esplicitamente il metodo drop. Quindi, se dobbiamo forzare la de-allocazione anticipata di un valore, usiamo la funzione std::mem::drop.

La funzione std::mem::drop è diversa dal metodo drop nel trait Drop. La chiamiamo passando come argomento il valore di cui vogliamo forzare il rilascio. La funzione si trova nel preludio, quindi possiamo modificare main nel Listato 15-15 per chiamare la funzione drop, come mostrato nel Listato 15-16.

File: src/main.rs
struct MioSmartPointer {
    data: String,
}

impl Drop for MioSmartPointer {
    fn drop(&mut self) {
        println!("Pulizia MioSmartPointer con dati `{}`!", self.data);
    }
}

fn main() {
    let c = MioSmartPointer {
        data: String::from("alcuni dati"),
    };
    println!("MioSmartPointer creato.");
    drop(c);
    println!("MioSmartPointer pulito prima della fine di main.");
}
Listato 15-16: Chiamata a std::mem::drop per eliminare esplicitamente un valore prima che esca dallo scope

L’esecuzione di questo codice stamperà quanto segue:

$ cargo run
   Compiling esempio-drop v0.1.0 (file:///progetti/esempio-drop)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.83s
     Running `target/debug/esempio-drop`
MioSmartPointer creato.
Pulizia MioSmartPointer con dati `alcuni dati`!
MioSmartPointer pulito prima della fine di main.

Il testo Eliminazione di MioSmartPointer con dati `alcuni dati`! viene stampato tra MioSmartPointer creato. e MioSmartPointer pulito prima della fine di main., a dimostrazione del fatto che il codice del metodo drop è chiamato per eliminare c in quel punto.

Puoi utilizzare il codice specificato in un’implementazione del trait Drop in molti modi per rendere la de-allocazione comoda e sicura: ad esempio, è possibile utilizzarlo per creare il proprio allocatore di memoria! Con il trait Drop e il sistema di ownership di Rust, non è necessario ricordarsi di de-allocare perché Rust lo fa automaticamente.

Inoltre, non è necessario preoccuparsi dei problemi derivanti dalla de-allocazione accidentale di valori ancora in uso: il sistema di ownership che garantisce che i reference siano sempre validi garantisce anche che drop venga chiamato solo una volta quando il valore non è più in uso.

Ora che abbiamo esaminato Box<T> e alcune delle caratteristiche dei puntatori intelligenti, diamo un’occhiata ad alcuni altri puntatori intelligenti definiti nella libreria standard.