Garbage Collector chiamerà IDisposable.Dispose per me?


134

Il modello IDisposable .NET implica che se si scrive un finalizzatore e si implementa IDisposable, il finalizzatore deve chiamare esplicitamente Dispose. Questo è logico ed è quello che ho sempre fatto nelle rare situazioni in cui un finalizzatore è garantito.

Tuttavia, cosa succede se faccio solo questo:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

e non implementare un finalizzatore o altro. Il framework chiamerà il metodo Dispose per me?

Sì, mi rendo conto che questo sembra stupido, e tutta la logica implica che non lo farà, ma ho sempre avuto 2 cose nella parte posteriore della mia testa che mi hanno reso incerto.

  1. Qualcuno qualche anno fa una volta mi disse che in realtà avrebbe fatto questo, e quella persona aveva una solida esperienza di "conoscere le loro cose".

  2. Il compilatore / framework fa altre cose "magiche" a seconda delle interfacce che implementate (es: foreach, metodi di estensione, serializzazione basata su attributi, ecc.), Quindi ha senso che anche questa potrebbe essere "magica".

Mentre Ho letto un sacco di cose su di esso, e c'è stato un sacco di cose implicite, non sono mai stato in grado di trovare una definitiva risposta sì o no a questa domanda.

Risposte:


121

.Net Garbage Collector chiama il metodo Object.Finalize di un oggetto nella garbage collection. Per impostazione predefinita, questo non fa nulla e deve essere ignorato se si desidera liberare risorse aggiuntive.

Dispose NON viene chiamato automaticamente e deve essere esplicitamente chiamato se le risorse devono essere rilasciate, ad esempio all'interno di un blocco "utilizzo" o "prova finalmente"

vedere http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx per ulteriori informazioni


35
In realtà, non credo che il GC chiami Object.Finalize se non viene ignorato. L'oggetto è determinato per non avere effettivamente un finalizzatore e la finalizzazione è soppressa, il che lo rende più efficiente, in quanto l'oggetto non deve trovarsi nelle code di finalizzazione / freachable.
Jon Skeet,

7
Secondo MSDN: msdn.microsoft.com/en-us/library/… non è possibile "sovrascrivere" effettivamente il metodo Object.Finalize in C #, il compilatore genera un errore: Non sovrascrivere object.Finalize. Fornisci invece un distruttore. ; vale a dire che devi implementare un distruttore che funga effettivamente da finalizzatore. [appena aggiunto qui per completezza in quanto questa è la risposta accettata e molto probabilmente da leggere]
Sudhanshu Mishra,

1
Il GC non fa nulla per un oggetto che non ignora un Finalizzatore. Non viene inserito nella coda di finalizzazione e non viene chiamato alcun finalizzatore.
Dave Black,

1
@dotnetguy - anche se la specifica C # originale menziona un "distruttore", in realtà si chiama Finalizzatore - e la sua meccanica è completamente diversa da come funziona un vero "distruttore" per i linguaggi non gestiti.
Dave Black,

67

Voglio sottolineare il punto di Brian nel suo commento, perché è importante.

I finalizzatori non sono distruttori deterministici come in C ++. Come altri hanno sottolineato, non vi è alcuna garanzia di quando verrà chiamato e in effetti se si dispone di memoria sufficiente, se verrà mai chiamato.

Ma la cosa brutta dei finalizzatori è che, come ha detto Brian, fa sì che il tuo oggetto sopravviva a una raccolta di rifiuti. Questo può essere negativo. Perché?

Come forse saprai, il GC è suddiviso in generazioni - Gen 0, 1 e 2, più l'heap di oggetti di grandi dimensioni. Dividi è un termine generico: ottieni un blocco di memoria, ma ci sono puntatori di dove gli oggetti Gen 0 iniziano e finiscono.

Il processo di pensiero è che probabilmente userete molti oggetti che avranno vita breve. Quindi quelli dovrebbero essere facili e veloci per arrivare al GC: oggetti Gen 0. Quindi, quando c'è pressione della memoria, la prima cosa che fa è una raccolta Gen 0.

Ora, se ciò non risolve abbastanza la pressione, allora torna indietro e fa uno sweep di Gen 1 (rifare Gen 0), e quindi se ancora non abbastanza, fa uno sweep di Gen 2 (rifare Gen 1 e Gen 0). Quindi la pulizia di oggetti di lunga durata può richiedere del tempo ed essere piuttosto costosa (poiché i fili potrebbero essere sospesi durante l'operazione).

Ciò significa che se fai qualcosa del genere:

~MyClass() { }

Il tuo oggetto, indipendentemente da cosa, vivrà alla seconda generazione. Questo perché GC non ha modo di chiamare il finalizzatore durante la raccolta dei rifiuti. Quindi gli oggetti che devono essere finalizzati vengono spostati in una coda speciale per essere ripuliti da un thread diverso (il thread del finalizzatore - che se uccidi fa accadere tutti i tipi di cose cattive). Ciò significa che i tuoi oggetti restano in giro più a lungo e potenzialmente costringono più raccolte di rifiuti.

Quindi, tutto ciò è solo per portare a casa il punto in cui si desidera utilizzare IDisposable per ripulire le risorse ogni volta che è possibile e provare seriamente a trovare modi per utilizzare il finalizzatore. È nell'interesse della tua applicazione.


8
Sono d'accordo che vuoi usare IDisposable ogni volta che è possibile, ma dovresti anche avere un finalizzatore che chiama un metodo di smaltimento. È possibile chiamare GC.SuppressFinalize () in IDispose.Dispose dopo aver chiamato il metodo dispose per assicurarsi che l'oggetto non venga inserito nella coda del finalizzatore.
jColeson,

2
Le generazioni sono numerate 0-2, non 1-3, ma il tuo post è comunque buono. Vorrei aggiungere ad esso, tuttavia, che qualsiasi oggetto a cui fa riferimento il tuo oggetto, o qualsiasi oggetto a cui fa riferimento quello, ecc. Sarà anche protetto contro la garbage collection (sebbene non contro la finalizzazione) per un'altra generazione. Pertanto, gli oggetti con finalizzatori non devono contenere riferimenti a qualcosa di non necessario per la finalizzazione.
supercat


3
Per quanto riguarda "Il tuo oggetto, indipendentemente da cosa, vivrà alla seconda generazione". Queste sono informazioni MOLTO fondamentali! Ha risparmiato molto tempo nel debug di un sistema, dove c'erano molti oggetti Gen2 di breve durata "preparati" per la finalizzazione, ma mai finalizzati causavano OutOfMemoryException a causa del pesante utilizzo dell'heap. Rimuovendo il finalizzatore (anche vuoto) e spostando (lavorando) il codice altrove, il problema è scomparso e il GC è stato in grado di gestire il carico.
temperino

@CoryFoy "Il tuo oggetto, indipendentemente da cosa, vivrà alla seconda generazione" Esiste una documentazione per questo?
Ashish Negi,

33

Ci sono già molte buone discussioni qui, e sono un po 'in ritardo alla festa, ma volevo aggiungere alcuni punti da solo.

  • Il Garbage Collecter non eseguirà mai direttamente un metodo Dispose per te.
  • Il GC eseguirà i finalizzatori quando ne avrà voglia.
  • Un modello comune che viene utilizzato per gli oggetti che hanno un finalizzatore è di chiamarlo un metodo che è per convenzione definito come Dispose (disposizione bool) che passa false per indicare che la chiamata è stata effettuata a causa della finalizzazione piuttosto che una chiamata Dispose esplicita.
  • Questo perché non è sicuro fare ipotesi su altri oggetti gestiti durante la finalizzazione di un oggetto (potrebbero essere già stati finalizzati).

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

Questa è la versione semplice, ma ci sono molte sfumature che possono farti inciampare su questo schema.

  • Il contratto per IDisposable.Dispose indica che deve essere sicuro chiamare più volte (chiamando Dispose su un oggetto che era già stato eliminato non dovrebbe fare nulla)
  • Può diventare molto complicato gestire correttamente una gerarchia di ereditarietà di oggetti usa e getta, specialmente se livelli diversi introducono nuove risorse usa e getta e non gestite. Nel modello sopra Dispose (bool) è virtuale per consentirne l'override in modo che possa essere gestito, ma trovo che sia soggetto a errori.

A mio avviso, è molto meglio evitare completamente di avere qualsiasi tipo che contenga direttamente sia riferimenti usa e getta che risorse native che potrebbero richiedere la finalizzazione. SafeHandles fornisce un modo molto pulito di farlo incapsulando le risorse native in usa e getta che forniscono internamente la propria finalizzazione (insieme a una serie di altri vantaggi come la rimozione della finestra durante P / Invoke in cui un handle nativo potrebbe andare perso a causa di un'eccezione asincrona) .

La semplice definizione di SafeHandle rende questo Trivial:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Ti consente di semplificare il tipo di contenimento per:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

1
Da dove viene la classe SafeHandleZeroOrMinusOneIsInvalid? È un tipo .net incorporato?
Orion Edwards,

+1 per // Secondo me, è molto meglio evitare completamente di avere qualsiasi tipo che contenga direttamente sia riferimenti usa e getta che risorse native che potrebbero richiedere la finalizzazione. // Le uniche classi non sigillate che dovrebbero mai avere finalizzatori sono quelle il cui scopo si concentra su finalizzazione.
supercat


1
Per quanto riguarda la chiamata a GC.SuppressFinalizein questo esempio. In questo contesto, SuppressFinalize dovrebbe essere chiamato solo se Dispose(true)eseguito correttamente. Se Dispose(true)a un certo punto si verifica un errore dopo la soppressione della finalizzazione, ma prima che tutte le risorse (in particolare quelle non gestite) vengano ripulite, è comunque necessario eseguire la finalizzazione per eseguire la maggior pulizia possibile. Meglio spostare la GC.SuppressFinalizechiamata nel Dispose()metodo dopo la chiamata a Dispose(true). Consulta le Linee guida per la progettazione del framework e questo post .
BitMask777

6

Io non la penso così. Hai il controllo su quando viene chiamato Dispose, il che significa che in teoria potresti scrivere un codice di smaltimento che fa ipotesi sull'esistenza di (ad esempio) altri oggetti. Non hai alcun controllo su quando viene chiamato il finalizzatore, quindi sarebbe incerto se il finalizzatore chiamasse automaticamente Dispose per tuo conto.


EDIT: sono andato via e testato, solo per essere sicuro:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

Fare ipotesi sugli oggetti disponibili durante lo smaltimento può essere pericoloso e complicato, specialmente durante la finalizzazione.
Scott Dorman,

3

Non nel caso che descrivi, ma il GC chiamerà il Finalizer per te, se ne hai uno.

PERÒ. La prossima garbage collection, invece di essere raccolta, l'oggetto andrà nella coda di finalizzazione, tutto verrà raccolto, quindi verrà chiamato finalizzatore. La raccolta successiva verrà liberata.

A seconda della pressione della memoria della tua app, potresti non avere un gc per quella generazione di oggetti per un po '. Quindi, nel caso di un flusso di file o una connessione db, potrebbe essere necessario attendere qualche istante affinché la risorsa non gestita venga liberata nella chiamata del finalizzatore per un po ', causando alcuni problemi.


1

No, non si chiama.

Ma questo rende facile non dimenticare di smaltire i tuoi oggetti. Usa la usingparola chiave.

Ho fatto il seguente test per questo:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

1
Questo è stato un esempio di come se NON usi la parola chiave <code> using </code> non verrà chiamata ... e questo frammento ha 9 anni, buon compleanno!
penyaskito,

1

Il GC non chiamerà dispose. Si può chiamare il finalizzatore, ma anche questo non è garantito in tutte le circostanze.

Vedi questo articolo per una discussione sul modo migliore per gestirlo.


0

La documentazione su IDisposable fornisce una spiegazione abbastanza chiara e dettagliata del comportamento, nonché un codice di esempio. Il GC NON chiamerà il Dispose()metodo sull'interfaccia, ma chiamerà il finalizzatore per il tuo oggetto.


0

Il modello IDisposable è stato creato principalmente per essere chiamato dallo sviluppatore, se si dispone di un oggetto che implementa IDispose, lo sviluppatore deve implementare la usingparola chiave nel contesto dell'oggetto o chiamare direttamente il metodo Dispose.

Il fail safe per il modello è implementare il finalizzatore chiamando il metodo Dispose (). In caso contrario, è possibile che si verifichino perdite di memoria, ovvero: Se si crea un wrapper COM e non si chiama mai System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (che verrebbe inserito nel metodo Dispose).

Non c'è magia nel clr per chiamare automaticamente i metodi Dispose se non quello di rintracciare oggetti che contengono finalizzatori e memorizzarli nella tabella Finalizzatori dal GC e chiamarli quando una euristica di pulizia avvia il GC.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.