Il punto di disporre è liberare risorse non gestite. Deve essere fatto a un certo punto, altrimenti non verranno mai ripuliti. Il garbage collector non sa come chiamare DeleteHandle()
una variabile di tipo IntPtr
, non sa se deve chiamare o meno DeleteHandle()
.
Nota : che cos'è una risorsa non gestita ? Se l'hai trovato in Microsoft .NET Framework: è gestito. Se hai cercato MSDN da solo, non è gestito. Qualsiasi cosa tu abbia usato le chiamate P / Invoke per uscire dal bel mondo confortevole di tutto ciò che ti è disponibile in .NET Framework non è gestito - e ora sei responsabile della pulizia.
L'oggetto che hai creato deve esporre un metodo che il mondo esterno può chiamare per ripulire le risorse non gestite. Il metodo può essere nominato come preferisci:
public void Cleanup()
o
public void Shutdown()
Ma invece esiste un nome standardizzato per questo metodo:
public void Dispose()
È stata persino creata un'interfaccia IDisposable
, che ha solo un metodo:
public interface IDisposable
{
void Dispose()
}
Quindi fai in modo che il tuo oggetto esponga l' IDisposable
interfaccia e in questo modo prometti di aver scritto quel singolo metodo per ripulire le risorse non gestite:
public void Dispose()
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}
E hai finito. Solo che puoi fare di meglio.
Cosa succede se il tuo oggetto ha allocato un System.Drawing.Bitmap da 250 MB (ovvero la classe Bitmap gestita .NET) come una sorta di frame buffer? Certo, questo è un oggetto .NET gestito e il garbage collector lo libererà. Ma vuoi davvero lasciare 250 MB di memoria semplicemente seduto lì - in attesa che il garbage collector alla fine arrivi e lo liberi? Cosa succede se è presente una connessione al database aperta ? Sicuramente non vogliamo che quella connessione rimanga aperta, in attesa che il GC finalizzi l'oggetto.
Se l'utente ha chiamato Dispose()
(nel senso che non ha più intenzione di utilizzare l'oggetto) perché non liberarsi di quelle bitmap e connessioni al database dispendiose?
Quindi ora faremo:
- sbarazzarsi di risorse non gestite (perché dobbiamo), e
- sbarazzarsi delle risorse gestite (perché vogliamo essere utili)
Quindi aggiorniamo il nostro Dispose()
metodo per sbarazzarci di quegli oggetti gestiti:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
E tutto va bene, tranne che puoi fare di meglio !
Cosa succede se la persona ha dimenticato di chiamare Dispose()
il tuo oggetto? Quindi avrebbero perso alcune risorse non gestite !
Nota: non perderanno risorse gestite , perché alla fine il Garbage Collector verrà eseguito, su un thread in background, e libererà la memoria associata a qualsiasi oggetto non utilizzato. Ciò comprende l'oggetto, ed eventuali oggetti gestiti che si utilizzano (per esempio il Bitmap
e il DbConnection
).
Se la persona ha dimenticato di chiamare Dispose()
, possiamo ancora salvare la loro pancetta! Abbiamo ancora un modo per chiamarlo per loro: quando il garbage collector finalmente riesce a liberare (cioè finalizzare) il nostro oggetto.
Nota: il Garbage Collector alla fine libererà tutti gli oggetti gestiti. Quando lo fa, chiama il Finalize
metodo sull'oggetto. Il GC non è a conoscenza o si preoccupa del metodo di smaltimento . Era solo un nome che abbiamo scelto per un metodo che chiamiamo quando vogliamo sbarazzarci di cose non gestite.
La distruzione del nostro oggetto da parte del Garbage Collector è il momento perfetto per liberare quelle fastidiose risorse non gestite. Lo facciamo sostituendo il Finalize()
metodo.
Nota: in C #, non si sostituisce esplicitamente il Finalize()
metodo. Scrivi un metodo che assomiglia a un distruttore C ++ e il compilatore lo considera l'implementazione del Finalize()
metodo:
~MyObject()
{
//we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
Dispose(); //<--Warning: subtle bug! Keep reading!
}
Ma c'è un bug in quel codice. Vedete, il Garbage Collector viene eseguito su un thread in background ; non conosci l'ordine in cui due oggetti vengono distrutti. È del tutto possibile che nel tuo Dispose()
codice, l' oggetto gestito che stai cercando di eliminare (perché volevi essere utile) non sia più presente:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
//Free managed resources too
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
this.frameBufferImage = null;
}
}
Quindi ciò di cui hai bisogno è un modo per Finalize()
dire Dispose()
che non dovrebbe toccare alcuna risorsa gestita (perché potrebbero non esserci più), pur liberando risorse non gestite.
Lo schema standard per fare questo è avere Finalize()
ed Dispose()
entrambi chiamare un terzo metodo (!); dove passi un booleano dicendo se lo stai chiamando Dispose()
(invece di Finalize()
), il che significa che è sicuro liberare risorse gestite.
A questo metodo interno potrebbe essere assegnato un nome arbitrario come "CoreDispose" o "MyInternalDispose", ma è tradizione chiamarlo Dispose(Boolean)
:
protected void Dispose(Boolean disposing)
Ma un nome di parametro più utile potrebbe essere:
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too, but only if I'm being called from Dispose
//(If I'm being called from Finalize then the objects might not exist
//anymore
if (itIsSafeToAlsoFreeManagedObjects)
{
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
}
E cambi l'implementazione del IDisposable.Dispose()
metodo in:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
}
e il tuo finalizzatore per:
~MyObject()
{
Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}
Nota : se il tuo oggetto discende da un oggetto che implementa Dispose
, non dimenticare di chiamare il metodo Dispose di base quando esegui l'override di Dispose:
public override void Dispose()
{
try
{
Dispose(true); //true: safe to free managed resources
}
finally
{
base.Dispose();
}
}
E tutto va bene, tranne che puoi fare di meglio !
Se l'utente chiama Dispose()
il tuo oggetto, allora tutto è stato ripulito. Più tardi, quando arriva il garbage collector e chiama Finalize, chiamerà di Dispose
nuovo.
Questo non è solo uno spreco, ma se il tuo oggetto ha riferimenti spazzatura a oggetti che hai già eliminato dall'ultima chiamata Dispose()
, proverai a smaltirli di nuovo!
Noterai nel mio codice che sono stato attento a rimuovere i riferimenti agli oggetti che ho eliminato, quindi non provo a chiamare Dispose
un riferimento a oggetti spazzatura. Ma ciò non ha impedito a un insetto sottile di insinuarsi.
Quando l'utente chiama Dispose()
: l'handle CursorFileBitmapIconServiceHandle viene distrutto. Più tardi, quando il garbage collector viene eseguito, tenterà di distruggere di nuovo lo stesso handle.
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy
...
}
Il modo in cui lo risolvi è dire al garbage collector che non ha bisogno di preoccuparsi di finalizzare l'oggetto: le sue risorse sono già state ripulite e non è necessario altro lavoro. Puoi farlo chiamando GC.SuppressFinalize()
il Dispose()
metodo:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}
Ora che l'utente ha chiamato Dispose()
, abbiamo:
- risorse non gestite liberate
- risorse gestite liberate
Nel GC non ha senso eseguire il finalizzatore: tutto è curato.
Non potrei usare Finalize per ripulire le risorse non gestite?
La documentazione per Object.Finalize
dice:
Il metodo Finalize viene utilizzato per eseguire operazioni di pulizia su risorse non gestite trattenute dall'oggetto corrente prima che l'oggetto venga distrutto.
Ma la documentazione MSDN dice anche, per IDisposable.Dispose
:
Esegue attività definite dall'applicazione associate alla liberazione, al rilascio o al ripristino di risorse non gestite.
Quindi che cos'è? Qual è il posto per ripulire le risorse non gestite? La risposta è:
È la vostra scelta! Ma scegli Dispose
.
Certamente potresti mettere la tua pulizia non gestita nel finalizzatore:
~MyObject()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//A C# destructor automatically calls the destructor of its base class.
}
Il problema è che non hai idea di quando il garbage collector riuscirà a finalizzare il tuo oggetto. I suoi non-gestita, non necessari, un-utilizzato risorse native saranno restare fino a quando il garbage collector alla fine viene eseguito. Quindi chiamerà il tuo metodo finalizzatore; ripulire le risorse non gestite. La documentazione di Object.Finalize sottolinea questo:
L'ora esatta in cui viene eseguito il finalizzatore non è definita. Per garantire il rilascio deterministico delle risorse per le istanze della classe, implementare un metodo Close o fornire IDisposable.Dispose
un'implementazione.
Questa è la virtù dell'utilizzo Dispose
per ripulire le risorse non gestite; puoi conoscere e controllare quando le risorse non gestite vengono ripulite. La loro distruzione è "deterministica" .
Per rispondere alla domanda originale: perché non rilasciare la memoria ora, piuttosto che quando GC decide di farlo? Ho un software di riconoscimento facciale che necessità di sbarazzarsi di 530 MB di immagini interne ora , dal momento che non sono più necessari. Quando non lo facciamo: la macchina si arresta in modo irreversibile.
Lettura bonus
Per chiunque ami lo stile di questa risposta (spiegando il perché , quindi il come diventa ovvio), ti suggerisco di leggere il capitolo 1 della COM essenziale di Don Box:
In 35 pagine spiega i problemi dell'uso di oggetti binari e inventa COM davanti ai tuoi occhi. Una volta compreso il perché di COM, le restanti 300 pagine sono ovvie e descrivono in dettaglio l'implementazione di Microsoft.
Penso che ogni programmatore che abbia mai avuto a che fare con oggetti o COM dovrebbe, per lo meno, leggere il primo capitolo. È la migliore spiegazione di qualsiasi cosa in assoluto.
Lettura bonus extra
Quando tutto quello che sai è sbagliato di Eric Lippert
È quindi molto difficile scrivere un finalizzatore corretto e il miglior consiglio che posso darti è di non provarci .