Cosas que deberías saber sobre los destructores en .NET

Este post es la continuación de Cosas que deberías saber sobre el Garbage Collector de .NET

¿Para qué necesitamos destructores en .NET? Como vimos en el anterior post, en .NET no tenemos que preocuparnos de destruir ni de liberar el espacio de los objetos dado que el GC se ocupa de esta tarea. No obstante, con mucha frecuencia nuestras aplicaciones mantienen referencias a objetos nativos (es decir, no .NET), como por ejemplo handles a ficheros, conexiones a bases de datos, etc. El GC sólo entiende de objetos .NET, y por lo tanto queda en manos del desarrollador asegurarse de que su código limpia y destruye adecuadamente los recursos nativos de los que hace uso.

Para asegurarnos que nuestros objetos .NET limpian toda la memoria no manejada cuando su vida finalice, tenemos que hacer dos cosas. La primera es que nuestra clase implemente la interfaz IDisposable y por tanto el método Dispose() . Este método debe ocuparse de realizar las tareas de limpieza de recursos no manejados, y finalmente llamar al método SuppressFinalize , enseguida veremos porqué.

GC.SuppressFinalize(this);

Para cierto tipo de objetos, el método Dispose() se le llama con otro nombre por motivos puramente semánticos. Por ejemplo, la clase SqlConnection tiene un método Close() y un método Dispose() que son funcionalmente equivalentes. En el caso de conexiones a base de datos, para la mayoría de los desarrolladores resulta más intuitivo llamara a Close() a la hora de cerrar la conexión y liberar los recursos asociados.

En C# (si no me equivoco VB no tiene nada equivalente) existe la instrucción using que nos permite establecer un contexto en el que se va a utilizar un objeto .NET. Al salir de dicho contexto (cuando llegamos a la llave de cierre ‘}’) se llamará automáticamente al método Dispose() del objeto en cuestión. En el siguiente ejemplo, al salir del contexto llamaríamos al método Dispose() del objeto fs:

string path = @"c:\temp\MyTest.txt";

using (FileStream fs = File.Create(path))

{

    byte[] info = new UTF8Encoding(true).GetBytes("This is some text");

    fs.Write(info, 0, info.Length);

}

El problema de la interfaz IDisposable es que dependemos de que los desarrolladores que consuman nuestras clases se acuerden de llamar a los métodos Dispose() o Close(), porque de lo contrario todo el esfuerzo habrá sido inútil. En previsión de que a los desarrolladores a veces se les pueda olvidar deshacerse correctamente de sus objetos, existen los destructores (en C#) o método Finalize() (en VB). En adelante me referiré a ambos cómo método Finalize() , pero son equivalentes y simplemente tienen nombres distintos por motivos lingüísticos.

Por lo tanto, la segunda de las cosas que debemos hacer es precisamente implementar un método Finalize() . El método Finalize() es un mecanismo que nos proporciona .NET para ejecutar código de finalización de un objeto de forma automática, antes de que el GC libere su memoria.

Uno de los principios básicos de .NET es que el código del método Finalize() de una clase debe ser muy rápido, nunca puede lanzar una excepción ni tampoco bloquear la ejecución. Los métodos Finalize() de todos los objetos de un procesose ejecutan en un mismo thread conocido como Finalizer. Cuando el GC determina que los objetos están listos para ser finalizados los mete en una cola y dicho thread ejecuta los métodos Finalize() de cada objeto secuencialmente. Hasta que no termina la ejecución del método Finalize() de un objeto, no comienza con el siguiente. Por lo tanto si se produce un bloqueo durante la finalización de alguno de los objetos, todo lo que venga detrás nunca será finalizado con todas las consecuencias que ello conlleva. Adicionalmente, en ASP.NET, este thread es el único que no tiene un manejador de excepciones predeterminado, por lo que cualquier excepción en este thread provocaría la finalización inmediata del proceso (también conocido como crash).

Para maximizar el rendimiento de nuestras aplicaciones .NET, debemos procurar que la menor cantidad posible de objetos sean finalizados por el GC. En el mejor de los casos, el método Finalize() no sería llamado nunca, sino que todos los objetos serían finalizados mediante la interfaz IDisposable . Para que os hagáis una idea de la penalización para el rendimiento que supone que un objeto sea finalizado por el GC, observad las diferencias en la tabla a continuación:

En definitiva, necesitamos implementar un destructor para nuestras clases, pero debemos procurar que dicho destructor no sea utilizado nunca. Espero que os haya convencido.

- Hasta el próximo post,

Daniel Mossberg