Curso de C# : Constructores
Constructores
El
constructor de una clase es un método que se encarga de ejecutar las
primeras acciones de un objeto cuando este se crea al instanciar la
clase. Estas acciones pueden ser: inicializar variables, abrir archivos,
asignar valores por defecto a las propiedades. Un par de reglas:
1º. El constructor ha de llamarse exactamente igual que la clase.
2º. El constructor nunca puede retornar un valor.
Lo
primero que se ejecuta al crear un objeto es el constructor de la clase
a la que dicho objeto pertenece (el compilador no exige que exista).
class Objeto
{
public Objeto()
{
Console.WriteLine("objeto instanciado");
}
}
class ConstructoresApp
{
static void Main()
{
Objeto o = new Objeto();
string a=Console.ReadLine();
}
}
En
este ejemplo, la clase Objeto tiene un constructor (en negrita). Se
declara igual que un método, con la salvedad de que no se pone ningún
tipo de retorno Al ejecutar este programa, la salida en la consola sería
esta:
objeto instanciado
En
el método Main no se ha dicho que escriba nada en la consola. Pero al
instanciar el objeto se ha ejecutado el constructor, y ha sido este el
que ha escrito esa línea en la consola.
Igual
que los métodos, los constructores también se pueden sobrecargar. Las
normas para hacerlo son las mismas: la lista de argumentos ha de ser
distinta en cada una de las sobrecargas. Se hace cuando se desea dar la
posibilidad de instanciar objetos de formas diferentes.
Por
otro lado, también existen los constructores estáticos (static). La
misión de estos constructores es inicializar los valores de los campos
static o hacer otras tareas necesarias para el funcionamiento de la
clase en el momento en que se haga el primer uso de ella.
Los
constructores static, no se pueden ejecutar más de una vez durante la
ejecución de un programa, y además la ejecución del mismo no puede ser
explícita, pues lo hará el compilador la primera vez que detecte que se
va a usar la clase.
El recolector de basura y los destructores
C#
incluye un recolector de basura (GC, Garbage Collector), es decir, ya
no es necesario liberar la memoria dinámica cuando no se necesita más
una referencia a un objeto. El GC no libera las referencias en el
momento en el que se dejan de utilizar, sino que lo hace siempre que se
produzca uno de estos tres casos:
Cuando no hay espacio suficiente en la memoria para meter un objeto que se pretende instanciar.
Cuando detecta que la aplicación va a finalizar su ejecución.
Cuando se invoca el método Collect de la clase System.GC.
Cuando
se instancia un objeto se reserva un bloque de memoria y se devuelve
una referencia (o puntero) al comienzo del mismo. Cuando este objeto
deja de ser utilizado (por ejemplo, estableciéndolo a null) lo que se
hace es destruir la referencia al mismo, pero el objeto permanece en el
espacio de memoria que estaba ocupando, y ese espacio de memoria no se
puede volver a utilizar hasta que no sea liberado. En C++ era tarea del
programador liberar estos espacios de memoria, y para ello se utilizaban
los destructores. Sin embargo, en C# esto es tarea del GC.
El
hecho de que tengamos un GC no quiere decir que los destructores dejen
de existir. Podemos incluir un método que se ocupe de realizar las otras
tareas de finalización, como eliminar archivos temporales que estuviera
utilizando el objeto, por poner un ejemplo. Lo que ocurre realmente
cuando escribimos un destructor es que el compilador sobre-escribe la
función virtual Finalize de la clase System.Object, colocando en ella el
código que hemos incluido en el destructor, y lo que invoca realmente
el GC cuando va a liberar la memoria de un objeto es este método
Finalize.
Los
destructores de C# no pueden ser invocados explícitamente como si
fueran un método más. Tampoco sirve invocar el método Finalize, ya que
su modificador de acceso es protected. Los destructores serán invocados
por el GC cuando este haga la recolección de basura. Como consecuencia,
los destructores no admiten modificadores de acceso ni argumentos,
tampoco pueden ser sobrecargados. Se han de nombrar igual que la clase,
precedidos por el signo ~.
class Objeto
{
~Objeto()
{
Console.WriteLine("Objeto liberado");
}
}
El destructor se llama igual que la clase precedido con el signo ~ (ALT 126). Ejemplo completo:
class Objeto
{
~Objeto()
{
Console.WriteLine("Referencia liberada");
}
}
class DestructoresApp
{
static void Main()
{
Objeto o=new Objeto();
}
}
El
programa no hace casi nada: instancia la clase Objeto y finaliza
inmediatamente. Al terminar, es cuando el GC entra en acción y ejecuta
el destructor de la clase Objeto, por lo que la salida en la consola
sería la siguiente:
Referencia liberada
Vamos a volver a poner el método Main con la línea Console.ReadLine:
static void Main()
{
Objeto o=new Objeto();
string a=Console.ReadLine();
}
Hay
dos motivos por los que no es necesario poner Console.ReadLine: el
primero es que no serviría de nada, puesto que el destructor no se
ejecutará hasta que el GC haga la recolección de basura, y esta no se
hará hasta que finalice la aplicación, y la aplicación finaliza después
de haber ejecutado todo el código del método Main. El segundo motivo es
que esto provocaría un error. Se produce el siguiente error: "No se
puede tener acceso a una secuencia cerrada". La primera vez que se
utiliza la clase Console se inicializan las secuencias de lectura y
escritura en la consola, y estas secuencias se cierran justo antes de
finalizar la aplicación. En el primer ejemplo funcionaría correctamente,
puesto que esta secuencia se inicia justamente en el destructor, ya que
antes de este no hay ninguna llamada a la clase Console. Sin embargo en
el segundo se produce un error, porque las secuencias se inician dentro
del método Main (al ejecutar Console.ReadLine), y se cierran cuando va a
finalizar el programa.
Los
hilos de ejecución del GC son de baja prioridad, de modo que, para
cuando el GC quiere ejecutar el destructor, las secuencias de escritura y
lectura de la consola ya han sido cerradas, y como los constructores
static no se pueden ejecutar más de una vez, la clase Console no puede
abrirlas por segunda vez.
Resurrección
Un
fenómeno curioso pero peligroso, que sucede con la recolección de
basura es la resurrección de objetos. No tiene mucha utilidad pero puede
generar problemas. Sucede cuando un objeto que va a ser eliminado
vuelve a crear una referencia a sí mismo durante la ejecución de su
destructor.
class Objeto
{
public int dato;
public Objeto(int valor)
{
this.dato=valor;
Console.WriteLine("Construido Objeto con el valor {0}",
valor);
}
~Objeto()
{
Console.WriteLine("Destructor de Objeto con el valor {0}",
this.dato);
ResurreccionApp.resucitado=this;
}
}
class ResurreccionApp
{
static public Objeto resucitado;
static void Main()
{
string c;
Console.WriteLine("Pulsa INTRO para crear el objeto");
c=Console.ReadLine();
resucitado=new Objeto(1);
Console.WriteLine("Valor de resucitado.dato: {0}", resucitado.dato);
Console.WriteLine("Pulsa INTRO para ejecutar resucitado=null; GC.Collect()");
c=Console.ReadLine();
resucitado=null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Valor de resucitado.dato: {0}", resucitado.dato);
Console.WriteLine("Pulsa INTRO para ejecutar resucitado=null; GC.Collect()");
c=Console.ReadLine();
resucitado=null;
GC.Collect();
Console.WriteLine("Ejecutado resucitado=null; GC.Collect()");
c=Console.ReadLine();
}
}
Vemos la salida en la consola:
Pulsa INTRO para crear el objeto
Construido Objeto con el valor 1
Valor de resucitado.dato: 1
Pulsa INTRO para ejecutar resucitado=null; GC.Collect()
Destructor de Objeto con el valor 1
Valor de resucitado.dato: 1
Pulsa INTRO para ejecutar resucitado=null; GC.Collect()
Ejecutado resucitado=null; GC.Collect()
Al
instanciar el objeto se ejecuta el constructor del mismo pero se anula
la referencia y, por lo tanto, el GC determina que puede liberarlo y
ejecuta su destructor. Sin embargo, cuando volvemos a escribir el valor
del campo "dato" ¡este vuelve a aparecer! En efecto, el GC no lo liberó a
pesar de haber ejecutado su destructor, y lo más curioso es que el
motivo por el que no lo ha liberado no es que se haya creado una nueva
referencia al objeto en el destructor, además cuando destruimos la
referencia y forzamos la recolección por segunda vez el destructor no se
ha ejecutado. Sí se ha liberado, pero no se ha ejecutado el destructor.
En resumen: lo que ha ocurrido es que la primera vez que destruimos la
referencia y ejecutamos GC.Collect se ejecutó el destructor pero no se
liberó, y la segunda vez se liberó pero no se ejecutó el destructor. La
explicación es la siguiente: Cuando se instancia un objeto, el GC
comprueba si este tiene un destructor. En caso afirmativo, guarda un
puntero hacia el objeto en una lista de finalizadores.
Al
ejecutar la recolección, el GC determina qué objetos se pueden liberar,
y posteriormente comprueba en la lista de finalizadores cuáles de ellos
tenían destructor. Si hay alguno que lo tiene, el puntero se elimina de
esta lista y se pasa a una segunda lista, en la que se colocan, por lo
tanto, los destructores que se deben invocar. El GC, por último, libera
todos los objetos a los que el programa ya no hace referencia excepto
aquellos que están en esta segunda lista, ya que si lo hiciera no se
podrían invocar los destructores, y aquí acaba la recolección. Como
consecuencia, un objeto que tiene destructor no se libera en la primera
recolección en la que se detecte que ya no hay referencias hacia él,
sino en la siguiente, y este es el motivo por el que, en el ejemplo, el
objeto no se liberó en la primera recolección. Tras esto, un nuevo hilo
de baja prioridad del GC se ocupa de invocar los destructores de los
objetos que están en esta segunda lista, y elimina los punteros de ella
según lo va haciendo. La siguiente vez que anula la referencia y el
forzado de la recolección del ejemplo, el GC determinó que dicho objeto
se podía liberar y lo liberó, pero no ejecutó su destructor porque la
dirección del objeto ya no estaba ni en la lista de finalizadores ni en
la segunda lista.
Generaciones
El
GC, para mejorar su rendimiento, agrupa los objetos en diferentes
generaciones. Los últimos objetos que se construyen suelen ser los
primeros en dejar de ser utilizados. Cuando se abre un cuadro de diálogo
con alguna opción de menú, se crea otro objeto, pero el cuadro de
diálogo se cerrará primero. No será siempre así pero es muy frecuente.
Cuando el GC necesita memoria, no revisa toda la memoria para liberar
todo lo liberable, sino que libera primero todo lo que pueda de la
última generación, pues lo más probable es que sea aquí donde encuentre
más objetos inútiles.
Si
el GC consigue liberar lo que necesita, no mirará en las generaciones
anteriores, aunque haya objetos que se pudieran liberar, en caso de que
liberando toda la memoria posible no consiguiera el espacio que
necesita, trataría de liberar también espacio de la siguiente generación
y, así sucesivamente. El GC agrupa un máximo de tres generaciones.
Comentarios
Publicar un comentario