Al igual que es posible definir
métodos constructores que incluyan código que gestione la creación de objetos
de un tipo de dato, también es posible definir un destructor que gestione cómo se destruyen los objetos de ese tipo
de dato. Este método suele ser útil para liberar recursos tales como los
ficheros o las conexiones de redes abiertas que el objeto a destruir estuviese
acaparando en el momento en que se fuese a destruir.
La destrucción de un objeto es
realizada por el recolector de basura cuando realiza una recolección de basura y detecta que no existen referencias a ese objeto
ni en pila, ni en registros ni desde otros objetos sí referenciados. Las
recolecciones se inician automáticamente cuando el recolector detecta que queda
poca memoria libre o que se va a finalizar la ejecución de la aplicación,
aunque también puede forzarse llamando al método Collect() de la clase System.GC
La sintaxis que se usa para
definir un destructor es la siguiente:
~<nombreTipo>()
{
<código>
}
Tras la ejecución del destructor
de un objeto de un determinado tipo siempre se llama al destructor de su tipo
padre, formándose así una cadena de llamadas a destructores que acaba al
llegarse al destructor de object. Éste último destructor no contiene código
alguno, y dado que object no tiene padre, tampoco llama a ningún
otro destructor.
Los destructores no se heredan.
Sin embargo, para asegurar que la cadena de llamadas a destructores funcione
correctamente si no incluimos ninguna definición de destructor en un tipo, el
compilador introducirá en esos casos una por nosotros de la siguiente forma:
~<nombreTipo>()
{}
El siguiente ejemplo muestra como
se definen destructores y cómo funciona la cadena de llamada a destructores:
using System;
class A
{
~A()
{
Console.WriteLine(“Destruido
objeto de clase A”);
}
}
class B:A
{
~B()
{
Console.WriteLine(“Destruido
objeto de clase B”);
}
public static void
Main()
{
new
B();
}
}
El código del método Main() de este programa crea un objeto de clase B pero no almacena ninguna referencia al mismo. Luego
finaliza la ejecución del programa, lo que provoca la actuación del recolector
de basura y la destrucción del objeto creado llamando antes a su destructor. La
salida que ofrece por pantalla el programa demuestra que tras llamar al
destructor de B se llama al
de su clase padre, ya que es:
Destruido objeto de
clase B
Destruido objeto de
clase A
Nótese que aunque no se haya
guardado ninguna referencia al objeto de tipo B
creado y por tanto sea innacesible para el programador, al recolector de basura
no le pasa lo mismo y siempre tiene acceso a los objetos, aunque sean inútiles
para el programador.
Es importante recalcar que no es
válido incluir ningún modificador en la definición de un destructor, ni
siquiera modificadores de acceso, ya que como nunca se le puede llamar explícitamente
no tiene ningún nivel de acceso para el programador. Sin embargo, ello no
implica que cuando se les llame no se tenga en cuenta el verdadero tipo de los
objetos a destruir, como demuestra el siguiente ejemplo:
using System;
public class Base
{
public virtual void F()
{
Console.WriteLine("Base.F");
}
~Base()
{
Console.WriteLine("Destructor
de Base");
this.F();
}
}
public class Derivada:Base
{
~Derivada()
{
Console.WriteLine("Destructor
de Derivada");
}
public override void
F()
{
Console.WriteLine("Derivada.F()");
}
public static void
Main()
{
Base
b = new Derivada();
}
}
La salida mostrada que muestra
por pantalla este programa al ejecutarlo es:
Destructor de
Derivada
Destructor de Base
Derivada.F()
Como se ve, aunque el objeto
creado se almacene en una variable de tipo Base,
su verdadero tipo es Derivada y por
ello se llama al destructor de esta clase al destruirlo. Tras ejecutarse dicho
contructor se llama al constructor de su clase padre siguiéndose la cadena de
llamadas a destructores. En este constructor padre hay una llamada al método
virtual F(), que como nuevamente
el objeto que se está destruyendo es de tipo Derivada,
la versión de F() a la que
se llamará es a la de la dicha clase.
Nótese que una llamada a un
método virtual dentro de un destructor como la que se hace en el ejemplo
anterior puede dar lugar a errores difíciles de detectar, pues cuando se llama
al método virtual ya se ha destruido la parte del objeto correspondiente al
tipo donde se definió el método ejecutado. Así, en el ejemplo anterior se ha
ejecutado Derivada.F() tras Derivada.~F(), por lo que si en Derivada.F() se usase algún campo destruido en Derivada.~F() podrían producirse errores difíciles de
detectar.