El punto de disposición es liberar recursos no administrados. Debe hacerse en algún momento, de lo contrario nunca se limpiarán. El recolector de basura no sabe cómo llamar DeleteHandle()
a una variable de tipo IntPtr
, no sabe si necesita o no llamar DeleteHandle()
.
Nota : ¿Qué es un recurso no administrado ? Si lo encontró en Microsoft .NET Framework: está administrado. Si te metiste a MSDN, no está administrado. Todo lo que haya utilizado para llamadas P / Invoke para salir del mundo cómodo y agradable de todo lo que tiene disponible en .NET Framework no está administrado, y ahora usted es responsable de limpiarlo.
El objeto que ha creado necesita exponer algún método, que el mundo exterior pueda llamar, para limpiar recursos no administrados. El método se puede nombrar como quieras:
public void Cleanup()
o
public void Shutdown()
Pero en cambio hay un nombre estandarizado para este método:
public void Dispose()
Incluso se creó una interfaz IDisposable
que tiene ese único método:
public interface IDisposable
{
void Dispose()
}
Entonces, hace que su objeto exponga la IDisposable
interfaz, y de esa manera promete que ha escrito ese método único para limpiar sus recursos no administrados:
public void Dispose()
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}
Y tu estas listo. Excepto que puedes hacerlo mejor.
¿Qué sucede si su objeto ha asignado un System.Drawing.Bitmap de 250 MB (es decir, la clase de mapa de bits administrado .NET) como algún tipo de búfer de cuadros? Claro, este es un objeto .NET administrado, y el recolector de basura lo liberará. Pero, ¿de verdad quieres dejar 250 MB de memoria simplemente sentado allí, esperando que el recolector de basura finalmente llegue y lo libere? ¿Qué pasa si hay una conexión de base de datos abierta ? Seguramente no queremos que esa conexión permanezca abierta, esperando que el GC finalice el objeto.
Si el usuario ha llamado Dispose()
(lo que significa que ya no planea usar el objeto), ¿por qué no deshacerse de esos mapas de bits y conexiones de bases de datos derrochadores?
Entonces ahora nosotros:
- deshacerse de los recursos no administrados (porque tenemos que hacerlo), y
- deshacerse de los recursos administrados (porque queremos ser útiles)
Así que vamos a actualizar nuestro Dispose()
método para deshacernos de esos objetos administrados:
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;
}
}
¡Y todo está bien, excepto que puedes hacerlo mejor !
¿Qué pasa si la persona olvidó llamar Dispose()
a su objeto? ¡Entonces perderían algunos recursos no administrados !
Nota: No perderán recursos administrados , porque eventualmente el recolector de basura se ejecutará, en un hilo de fondo, y liberará la memoria asociada con cualquier objeto no utilizado. Esto incluirá su objeto y cualquier objeto administrado que use (por ejemplo, el Bitmap
y el DbConnection
).
Si la persona olvidó llamar Dispose()
, ¡ aún podemos salvar su tocino! Todavía tenemos una forma de llamarlo para ellos: cuando el recolector de basura finalmente consigue en torno a la liberación (es decir finalización) nuestro objeto.
Nota: El recolector de basura eventualmente liberará todos los objetos administrados. Cuando lo hace, llama al Finalize
método en el objeto. El GC no sabe, ni le importa, acerca de su método de eliminación . Ese fue solo un nombre que elegimos para un método que llamamos cuando deseamos deshacernos de cosas no administradas.
La destrucción de nuestro objeto por parte del recolector de basura es el momento perfecto para liberar esos molestos recursos no administrados. Hacemos esto anulando el Finalize()
método.
Nota: En C #, no anula explícitamente el Finalize()
método. Escribe un método que se parece a un destructor de C ++ , y el compilador lo considera su implementación del Finalize()
método:
~MyObject()
{
//we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
Dispose(); //<--Warning: subtle bug! Keep reading!
}
Pero hay un error en ese código. Verá, el recolector de basura se ejecuta en un subproceso de fondo ; no sabes el orden en que se destruyen dos objetos. Es completamente posible que en su Dispose()
código, el objeto administrado del que está tratando de deshacerse (porque quería ser útil) ya no esté allí:
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;
}
}
Entonces, lo que necesita es una forma de Finalize()
saber Dispose()
que no debe tocar ningún recurso administrado (porque es posible que ya no esté allí ), sin dejar de liberar recursos no administrados.
El patrón estándar para hacer esto es tener Finalize()
y Dispose()
ambos llamar a un tercer método (!); donde pasa un refrán booleano si lo está llamando Dispose()
(en lugar de Finalize()
), lo que significa que es seguro liberar recursos administrados.
Este método interno podría recibir un nombre arbitrario como "CoreDispose" o "MyInternalDispose", pero es tradición llamarlo Dispose(Boolean)
:
protected void Dispose(Boolean disposing)
Pero un nombre de parámetro más útil podría ser:
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;
}
}
}
Y cambia su implementación del IDisposable.Dispose()
método a:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
}
y tu finalizador para:
~MyObject()
{
Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}
Nota : Si su objeto desciende de un objeto que se implementa Dispose
, no olvide llamar a su método Dispose básico cuando anule Dispose:
public override void Dispose()
{
try
{
Dispose(true); //true: safe to free managed resources
}
finally
{
base.Dispose();
}
}
¡Y todo está bien, excepto que puedes hacerlo mejor !
Si el usuario llama Dispose()
a su objeto, entonces todo se ha limpiado. Más tarde, cuando aparezca el recolector de basura y llame a Finalizar, volverá a llamar Dispose
.
No solo es un desperdicio, sino que si su objeto tiene referencias basura a objetos que ya eliminó de la última llamada Dispose()
, ¡intentará eliminarlos nuevamente!
Notarás en mi código que tuve cuidado de eliminar las referencias a los objetos que he dispuesto, por lo que no trato de invocar Dispose
una referencia de objeto basura. Pero eso no impidió que un error sutil se deslizara.
Cuando el usuario llama Dispose()
: se destruye el identificador CursorFileBitmapIconServiceHandle . Más tarde, cuando se ejecuta el recolector de basura, intentará destruir el mismo controlador nuevamente.
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy
...
}
La forma de solucionar esto es decirle al recolector de basura que no necesita molestarse en finalizar el objeto: sus recursos ya se han limpiado y no se necesita más trabajo. Para ello, llame GC.SuppressFinalize()
al Dispose()
método:
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
}
Ahora que el usuario ha llamado Dispose()
, tenemos:
- recursos no gestionados liberados
- recursos gestionados liberados
No tiene sentido que el GC ejecute el finalizador: todo se ha solucionado.
¿No podría usar Finalizar para limpiar recursos no administrados?
La documentación para Object.Finalize
dice:
El método Finalizar se utiliza para realizar operaciones de limpieza en los recursos no administrados retenidos por el objeto actual antes de que el objeto sea destruido.
Pero la documentación de MSDN también dice, para IDisposable.Dispose
:
Realiza tareas definidas por la aplicación asociadas con la liberación, liberación o restablecimiento de recursos no administrados.
Entonces cual es? ¿Cuál es el lugar para limpiar los recursos no administrados? La respuesta es:
¡Es tu elección! Pero elige Dispose
.
Ciertamente, podría colocar su limpieza no administrada en el finalizador:
~MyObject()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//A C# destructor automatically calls the destructor of its base class.
}
El problema con esto es que no tienes idea de cuándo el recolector de basura se encargará de finalizar tu objeto. Sus recursos nativos no administrados, innecesarios y no utilizados se quedarán hasta que el recolector de basura finalmente se ejecute. Entonces llamará a su método finalizador; limpiar recursos no administrados. La documentación de Object.Finalize señala esto:
La hora exacta en que se ejecuta el finalizador no está definida. Para garantizar la liberación determinista de recursos para instancias de su clase, implemente un método Close o proporcione una IDisposable.Dispose
implementación.
Esta es la virtud de usar Dispose
para limpiar recursos no administrados; puede saber y controlar cuándo se limpian los recursos no administrados. Su destrucción es "determinista" .
Para responder a su pregunta original: ¿Por qué no liberar memoria ahora, en lugar de cuando el GC decida hacerlo? Tengo un software de reconocimiento facial que necesita deshacerse de 530 MB de imágenes internas ahora , ya que ya no son necesarias. Cuando no lo hacemos: la máquina se detiene por completo.
Lectura adicional
Para cualquiera a quien le guste el estilo de esta respuesta (explicando el por qué , por lo que el cómo se vuelve obvio), le sugiero que lea el Capítulo Uno del COM esencial de Don Box:
En 35 páginas, explica los problemas del uso de objetos binarios e inventa COM ante sus ojos. Una vez que se da cuenta del por qué de COM, las 300 páginas restantes son obvias y solo detallan la implementación de Microsoft.
Creo que cada programador que alguna vez haya tratado con objetos o COM debería, como mínimo, leer el primer capítulo. Es la mejor explicación de cualquier cosa.
Lectura extra de bonificación
Cuando todo lo que sabes está mal por Eric Lippert
Por lo tanto, es muy difícil escribir un finalizador correcto, y el mejor consejo que puedo darle es no intentarlo .