¿El recolector de basura llamará a IDisposable?

134

El patrón .NET IDisposable implica que si escribe un finalizador e implementa IDisposable, su finalizador debe llamar explícitamente a Dispose. Esto es lógico, y es lo que siempre he hecho en las raras situaciones en las que se justifica un finalizador.

Sin embargo, qué sucede si solo hago esto:

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

y no implemente un finalizador, ni nada. ¿El marco llamará al método Dispose para mí?

Sí, me doy cuenta de que esto suena tonto, y toda lógica implica que no lo hará, pero siempre he tenido 2 cosas detrás de mi cabeza que me han hecho sentir inseguro.

  1. Hace unos años, alguien me dijo que, de hecho, haría esto, y que esa persona tenía un historial muy sólido de "conocer sus cosas".

  2. El compilador / marco hace otras cosas 'mágicas' dependiendo de qué interfaces implemente (por ejemplo: foreach, métodos de extensión, serialización basada en atributos, etc.), por lo que tiene sentido que esto también sea 'mágico'.

Si bien he leído muchas cosas al respecto, y ha habido muchas cosas implícitas, nunca he podido encontrar una respuesta definitiva de Sí o No a esta pregunta.

Orion Edwards
fuente

Respuestas:

121

El recolector de basura .Net llama al método Object.Finalize de un objeto en la recolección de basura. De forma predeterminada, esto no hace nada y debe anularse si desea liberar recursos adicionales.

Dispose NO se llama automáticamente y debe llamarse explícitamente si se van a liberar recursos, como dentro de un bloque 'usar' o 'intentar finalmente'

ver http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx para más información

Xian
fuente
35
En realidad, no creo que el GC llame a Object. Finalice en absoluto si no se anula. Se determina que el objeto efectivamente no tiene un finalizador, y se suprime la finalización, lo que lo hace más eficiente, ya que el objeto no necesita estar en la finalización / colas alcanzables.
Jon Skeet
77
Según MSDN: msdn.microsoft.com/en-us/library/… en realidad no puede "anular" el método Object.Finalize en C #, el compilador genera un error: no anule object.Finalize. En su lugar, proporcione un destructor. ; es decir, debe implementar un destructor que efectivamente actúe como Finalizador. [Recién agregado aquí para completar ya que esta es la respuesta aceptada y es más probable que se lea]
Sudhanshu Mishra
1
El GC no hace nada a un objeto que no anula un Finalizador. No se coloca en la cola de Finalización, y no se llama a Finalizer.
Dave Black el
1
@dotnetguy, aunque la especificación original de C # menciona un "destructor", en realidad se llama Finalizador, y su mecánica es completamente diferente de cómo funciona un verdadero "destructor" para lenguajes no administrados.
Dave Black el
67

Quiero enfatizar el punto de Brian en su comentario, porque es importante.

Los finalizadores no son destructores deterministas como en C ++. Como otros han señalado, no hay garantía de cuándo se llamará y, de hecho, si tiene suficiente memoria, si alguna vez se llamará.

Pero lo malo de los finalizadores es que, como dijo Brian, hace que su objeto sobreviva a una recolección de basura. Esto puede ser malo. ¿Por qué?

Como puede saber o no, el GC se divide en generaciones: Gen 0, 1 y 2, más el montón de objetos grandes. Dividir es un término suelto: obtienes un bloque de memoria, pero hay indicadores de dónde comienzan y terminan los objetos Gen 0.

El proceso de pensamiento es que probablemente usarás muchos objetos que durarán poco. Por lo tanto, deberían ser fáciles y rápidos para que el GC llegue a los objetos de Gen 0. Entonces, cuando hay presión de memoria, lo primero que hace es una colección Gen 0.

Ahora, si eso no resuelve suficiente presión, entonces regresa y realiza un barrido Gen 1 (rehaciendo Gen 0), y luego, si aún no es suficiente, realiza un barrido Gen 2 (rehaciendo Gen 1 y Gen 0). Por lo tanto, limpiar objetos de larga duración puede llevar un tiempo y ser bastante costoso (ya que sus hilos pueden estar suspendidos durante la operación).

Esto significa que si haces algo como esto:

~MyClass() { }

Su objeto, pase lo que pase, vivirá para la Generación 2. Esto se debe a que el GC no tiene forma de llamar al finalizador durante la recolección de basura. Por lo tanto, los objetos que deben finalizarse se mueven a una cola especial para que los limpie un subproceso diferente (el subproceso finalizador, que si mata hace que sucedan todo tipo de cosas malas). Esto significa que sus objetos permanecen más tiempo y potencialmente obligan a más recolecciones de basura.

Por lo tanto, todo eso es solo para llevar a casa el punto que desea usar IDisposable para limpiar recursos siempre que sea posible y tratar seriamente de encontrar formas de usar el finalizador. Es en el mejor interés de su aplicación.

Cory Foy
fuente
8
Estoy de acuerdo en que desea usar IDisposable siempre que sea posible, pero también debe tener un finalizador que llame a un método de eliminación. Puede llamar a GC.SuppressFinalize () en IDispose.Dispose después de llamar a su método de eliminación para asegurarse de que su objeto no se ponga en la cola del finalizador.
jColeson
2
Las generaciones están numeradas del 0 al 2, no del 1 al 3, pero tu publicación es buena. Sin embargo, agregaría que cualquier objeto al que haga referencia su objeto, o cualquier objeto al que hagan referencia esos, etc., también estará protegido contra la recolección de basura (aunque no contra la finalización) para otra generación. Por lo tanto, los objetos con finalizadores no deben contener referencias a nada que no sea necesario para la finalización.
supercat
Referencia (de un tipo) para números de generación
carreras de ligereza en órbita el
3
Con respecto a "Su objeto, pase lo que pase, vivirá hasta la Generación 2". ¡Esta es información MUY fundamental! Ahorró mucho tiempo en la depuración de un sistema, donde había muchos objetos Gen2 de corta duración "preparados" para la finalización, pero nunca finalizados causaron OutOfMemoryException debido al uso intensivo del montón. Al eliminar el finalizador (incluso vacío) y mover (trabajar) el código a otra parte, el problema desapareció y el GC pudo manejar la carga.
sacapuntas
@CoryFoy "Su objeto, pase lo que pase, vivirá para la Generación 2" ¿Hay alguna documentación para esto?
Ashish Negi
33

Ya hay mucha buena discusión aquí, y llego un poco tarde a la fiesta, pero yo mismo quería agregar algunos puntos.

  • El recolector de basura nunca ejecutará directamente un método Dispose para usted.
  • El GC se ejecutará finalizadores cuando se siente como él.
  • Un patrón común que se usa para los objetos que tienen un finalizador es hacer que llame a un método que, por convención, se define como Dispose (bool disposing) pasando false para indicar que la llamada se realizó debido a la finalización en lugar de una llamada explícita Dispose.
  • Esto se debe a que no es seguro hacer suposiciones sobre otros objetos administrados al finalizar un objeto (es posible que ya se hayan finalizado).

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;
  }
 }
}

Esa es la versión simple, pero hay muchos matices que pueden hacerte tropezar con este patrón.

  • El contrato para IDisposable.Dispose indica que debe ser seguro llamar varias veces (llamar a Dispose en un objeto que ya estaba dispuesto no debería hacer nada)
  • Puede ser muy complicado administrar adecuadamente una jerarquía de herencia de objetos desechables, especialmente si las diferentes capas introducen nuevos recursos desechables y no administrados. En el patrón anterior, Dispose (bool) es virtual para permitir que se anule para que pueda administrarse, pero creo que es propenso a errores.

En mi opinión, es mucho mejor evitar por completo tener cualquier tipo que contenga directamente referencias desechables y recursos nativos que puedan requerir finalización. SafeHandles proporciona una forma muy limpia de hacerlo al encapsular recursos nativos en desechables que proporcionan internamente su propia finalización (junto con una serie de otros beneficios, como eliminar la ventana durante P / Invoke, donde un controlador nativo podría perderse debido a una excepción asincrónica) .

Simplemente definir un SafeHandle hace que este Trivial:


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

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

Le permite simplificar el tipo que contiene a:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
Andrés
fuente
1
¿De dónde viene la clase SafeHandleZeroOrMinusOneIsInvalid? ¿Es un tipo incorporado en .net?
Orion Edwards
+1 para // En mi opinión, es mucho mejor evitar por completo tener cualquier tipo que contenga directamente referencias desechables y recursos nativos que puedan requerir finalización. // Las únicas clases sin sellar que deberían tener finalizadores son aquellas cuyo propósito se centra en finalización
supercat
1
@OrionEdwards sí, ver msdn.microsoft.com/en-us/library/…
Martin Capodici
1
Respecto a la llamada a GC.SuppressFinalizeen este ejemplo. En este contexto, SuppressFinalize solo debería llamarse si se Dispose(true)ejecuta con éxito. Si Dispose(true)falla en algún momento después de que se suprime la finalización pero antes de que se limpien todos los recursos (especialmente los no administrados), aún así desea que se realice la finalización para realizar la mayor limpieza posible. Es mejor mover la GC.SuppressFinalizellamada al Dispose()método después de la llamada a Dispose(true). Consulte las Pautas de diseño del marco y esta publicación .
BitMask777
6

No lo creo. Usted tiene control sobre cuándo se llama a Dispose, lo que significa que, en teoría, podría escribir un código de eliminación que haga suposiciones sobre (por ejemplo) la existencia de otros objetos. No tiene control sobre cuándo se llama al finalizador, por lo que sería dudoso que el finalizador llame automáticamente a Dispose en su nombre.


EDITAR: Me fui y probé, solo para asegurarme:

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");
    }
}
Matt Bishop
fuente
Hacer suposiciones sobre los objetos que están disponibles para usted durante la eliminación puede ser peligroso y engañoso, especialmente durante la finalización.
Scott Dorman
3

No en el caso que usted describe, pero el GC llamará al Finalizador por usted, si tiene uno.

SIN EMBARGO. La próxima recolección de basura, en lugar de ser recolectada, el objeto irá a la cola de finalización, todo se recolectará, luego se llamará finalizador. La próxima colección después de eso será liberada.

Dependiendo de la presión de memoria de su aplicación, es posible que no tenga un gc para la generación de ese objeto por un tiempo. Entonces, en el caso de, por ejemplo, una secuencia de archivos o una conexión de base de datos, es posible que tenga que esperar un tiempo para que el recurso no administrado se libere en la llamada del finalizador por un tiempo, causando algunos problemas.

Brian Leahy
fuente
1

No, no se llama.

Pero esto hace que no te olvides de deshacerte de tus objetos. Solo usa la usingpalabra clave.

Hice la siguiente prueba para esto:

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!");
    }
penyaskito
fuente
1
Este fue un ejemplo de cómo si NO usa la palabra clave <code> using </code>, no se llamará ... y este fragmento tiene 9 años, ¡feliz cumpleaños!
penyaskito
1

El GC no llamará a disponer. Se puede llamar a su finalizador, pero incluso esto no está garantizado en todas las circunstancias.

Vea este artículo para una discusión sobre la mejor manera de manejar esto.

Rob Walker
fuente
0

La documentación sobre IDisposable ofrece una explicación bastante clara y detallada del comportamiento, así como un código de ejemplo. El GC NO llamará al Dispose()método en la interfaz, pero llamará al finalizador para su objeto.

Joseph Daigle
fuente
0

El patrón IDisposable fue creado principalmente para ser llamado por el desarrollador, si tiene un objeto que implementa IDispose, el desarrollador debe implementar la usingpalabra clave alrededor del contexto del objeto o llamar al método Dispose directamente.

La seguridad del patrón es implementar el finalizador que llama al método Dispose (). Si no lo hace, puede crear algunas pérdidas de memoria, es decir: si crea algún contenedor COM y nunca llama al System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (que se colocaría en el método Dispose).

No hay magia en el clr para llamar a los métodos de eliminación automáticamente, aparte de rastrear objetos que contienen finalizadores y almacenarlos en la tabla de finalizadores por el GC y llamarlos cuando el GC activa algunas heurísticas de limpieza.

Erick Sgarbi
fuente