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
.
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."); }
MioSmartPointer
che implementa il trait Drop
dove inseriremo il nostro codice di de-allocazioneIl 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.
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.");
}
drop
del trait Drop
per una de-allocazione anticipataQuando 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.
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."); }
std::mem::drop
per eliminare esplicitamente un valore prima che esca dallo scopeL’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.