Constructores

Concepto de constructores

    Los constructores de un tipo de datos son métodos especiales que se definen como miembros de éste y que contienen código a ejecutar cada vez que se cree un objeto de ese tipo. Éste código suele usarse para labores de inicialización de los campos del objeto a crear, sobre todo cuando el valor de éstos no es constante o incluye acciones más allá de una asignación de valor (aperturas de ficheros, accesos a redes, etc.)

    Hay que tener en cuenta que la ejecución del constructor siempre se realiza después de haberse inicializado todos los campos del objeto, ya sea con los valores iniciales que se hubiesen especificado en su definición o dejándolos con el valor por defecto de su tipo.

    Aparte de su especial sintaxis de definición, los constructores y los métodos normales tienen una diferencia muy importante: los constructores no se heredan.

Definición de constructores

    La sintaxis básica de definición de constructores consiste en definirlos como cualquier otro método pero dándoles el mismo nombre que el tipo de dato al que pertenecen y no indicando el tipo de valor de retorno debido a que nunca pueden devolver nada. Es decir, se usa la sintaxis:


<modificadores> <nombreTipo>(<parámetros>)
{
 <código>
}

    Un constructor nunca puede devolver ningún tipo de objeto porque, como ya se ha visto, sólo se usa junto al operador new, que devuelve una referencia al objeto recién creado. Por ello, es absurdo que devuelva algún valor ya que nunca podría ser capturado en tanto que new nunca lo devolvería. Por esta razón el compilador considera erróneo indicar algún tipo de retorno en su definición, incluso aunque se indique void.

Llamada al constructor

    Al constructor de una clase se le llama en el momento en que se crea algún objeto de la misma usando el operador new. De hecho, la forma de uso de este operador es:


new <llamadaConstructor>

    Por ejemplo, el siguiente programa demuestra cómo al crearse un objeto se ejecutan las instrucciones de su constructor:


class Prueba
{
 Prueba(int x)
 {
  System.Console.Write("Creado objeto Prueba con x={0}",x);
 }
 public static void Main()
 {
  Prueba p = new Prueba(5);
 }
}

    La salida por pantalla de este programa demuestra que se ha llamado al constructor del objeto de clase Prueba creado en Main(), pues es:



Creado objeto Prueba con x=5;

 

Llamadas entre constructores

     Al igual que ocurre con cualquier otro método, también es posible sobrecargar los constructores. Es decir, se pueden definir varios constructores siempre y cuando éstos tomen diferentes números o tipos de parámetros. Además, desde el código de un constructor puede llamarse a otros constructores del mismo tipo de dato antes de ejecutar las instrucciones del cuerpo del primero. Para ello se añade un inicializador this al constructor, que es estructura que precede a la llave de apertura de su cuerpo tal y como se muestra en el siguiente ejemplo:


class A
{
 int total;
 
 A(int valor): this(valor, 2);  // (1)
 {
 }
 
 A(int valor, int peso) // (2)
 {
  total = valor*peso;
 }
}

     El this incluido hace que la llamada al constructor (1) de la clase A provoque una llamada al constructor (2) de esa misma clase en la que se le pase como primer parámetro el valor originalmente pasado al constructor (1) y como segundo parámetro el valor 2. Es importante señalar que la llamada al constructor (2) en (1) se hace antes de ejecutar cualquier instrucción de (1)

     Nótese que la sobrecarga de constructores -y de cualquier método en general- es un buen modo de definir versiones más compactas de métodos de uso frecuente en las que se tomen valores por defecto para parámetros de otras versiones menos compactas del mismo método. La implementación de estas versiones compactas consistiría en hacer una llamada a la versión menos compacta del método en la que se le pasen esos valores por defecto (a través del this en el caso de los constructores) y si acaso luego (y/o antes, si no es un constructor) se hagan labores específicas en el cuerpo del método compacto.

     Del mismo modo que en la definición de un constructor de un tipo de datos es posible llamar a otros constructores del mismo tipo de datos, también es posible hacer llamadas a constructores de su tipo padre sustituyendo en su inicializador la palabra reservada this por base. Por ejemplo:


class A
{
 int total;
 
 A(int valor, int peso)
 {
  total = valor*peso;
 }
}
class B:A
{
 B(int valor):base(valor,2)
 {}
}

    En ambos casos, los valores pasados como parámetros en el inicializador no pueden contener referencias a campos del objeto que se esté creando, ya que se considera que un objeto no está creado hasta que no se ejecute su constructor y, por tanto, al llamar al inicializador aún no está creado. Sin embargo, lo que sí pueden incluir son referencias a los parámetros con los que se llamó al constructor. Por ejemplo, sería válido hacer:


A(int x, int y): this(x+y)
{}

Constructor por defecto

    Todo tipo de datos ha de disponer de al menos un constructor. Cuando se define un tipo sin especificar ninguno el compilador considera que implícitamente se ha definido uno  sin cuerpo ni parámetros de la siguiente forma:


public <nombreClase>(): base()
{}

    En el caso de que el tipo sea una clase abstracta, entonces el constructor por defecto introducido es el que se muestra a continuación, ya que el anterior no sería válido porque permitiría crear objetos de la clase a la que pertenece:


protected <nombreClase>(): base()
{}

    En el momento en se defina explícitamente algún constructor el compilador dejará de introducir implícitamente el anterior. Hay que tener especial cuidado con la llamada que este constructor por defecto realiza en su inicializador, pues pueden producirse errores como el del siguiente ejemplo:


class A
{
 public A(int x)
 {}
}
class B:A
{
 public static void Main()
 {
  B b = new B();  // Error: No hay constructor base
 }
}

    En este caso, la creación del objeto de clase B en Main() no es posible debido a que el constructor que por defecto el compilador crea para la clase B llama al constructor sin parámetros de su clase base A, pero A carece de dicho constructor porque no se le ha definido explícitamente ninguno con esas características pero se le ha definido otro que ha hecho que el compilador no le defina implícitamente el primero.

    Otro error que podría darse consistiría en que aunque el tipo padre tuviese un constructor sin parámetros, éste fuese privado y por tanto inaccesible para el tipo hijo.

    También es importante señalar que aún en el caso de que definamos nuestras propios constructores, si no especificamos un inicializador el compilador introducirá por nosotros uno de la forma :base() Por tanto, en estos casos también hay que asegurarse de que el tipo donde se haya definido el constructor herede de otro que tenga un constructor sin parámetros no privado.

Llamadas polimórficas en constructores

    Es conveniente evitar en la medida de lo posible la realización de llamadas a métodos virtuales dentro de los constructores, ya que ello puede provocar errores muy difíciles de detectar debido a que se ejecuten métodos cuando la parte del objeto que manipulan aún no se ha sido inicializado. Un ejemplo de esto es el siguiente:


using System;
public class Base
{
 public Base()
 {
  Console.WriteLine("Constructor de Base");
  this.F();
 }
 
 public virtual void F()
 {
  Console.WriteLine("Base.F");               
 }
}
public class Derivada:Base
{
 Derivada()
 {
  Console.WriteLine("Constructor de Derivada");
 }
 
 public override void F()
 {
  Console.WriteLine("Derivada.F()");
 }
 
 public static void Main()
 {
  Base b = new Derivada();                      
 }
}

    La salida por pantalla mostrada por este programa al ejecutarse es la siguiente:

Constructor de Base

Derivada.F()

Constructor de Derivada

    Lo que ha ocurrido es lo siguiente: Al crearse el objeto Derivada se ha llamado a su constructor sin parámetros, que como no tiene inicializador implícitamente llama al constructor sin parámetros de su clase base. El constructor de Base realiza una llamada al método virtual F(), y como el verdadero tipo del objeto que se está construyendo es Derivada, entonces la versión del método virtual ejecutada es la redefinición del mismo incluida en dicha clase. Por último, se termina llamando al constructor de Derivada y finaliza la construcción del objeto.

    Nótese que se ha ejecutado el método F() de Derivada antes que el código del constructor de dicha clase, por lo que si ese método manipulase campos definidos en Derivada que se inicializasen a través de constructor, se habría accedido a ellos antes de inicializarlos y ello seguramente provocaría errores de causas difíciles de averiguar.

Constructor de tipo

    Todo tipo puede tener opcionalmente un constructor de tipo, que es un método especial que funciona de forma similar a los constructores ordinarios sólo que para lo que se usa es para inicializar los campos static del tipo donde se ha definido.

    Cada tipo de dato sólo puede tener un constructor de tipo. Éste constructor es llamado automáticamente por el compilador la primera vez que se accede al tipo, ya sea para crear objetos del mismo o para acceder a sus campos estáticos. Esta llamada se hace justo después de inicializar los campos estáticos del tipo con los valores iniciales especificados al definirlos (o, en su ausencia, con los valores por defecto de sus tipos de dato), por lo que el programador no tiene forma de controlar la forma en que se le llama y, por tanto, no puede pasarle parámetros que condicionen su ejecución.

    Como cada tipo sólo puede tener un constructor de tipo no tiene sentido poder usar this en su inicializador para llamar a otro. Y además, tampoco tiene sentido usar base debido a que éste siempre hará referencia al constructor de tipo sin parámetros de su  clase base. O sea, un constructor de tipo no puede tener inicializador.

    Además, no tiene sentido darle modificadores de acceso ya que el programador nunca lo podrá llamar sino que sólo será llamado automáticamente y sólo al accederse al tipo por primera vez. Como es absurdo, el compilador considera un error dárselos.

    La forma en que se define el constructor de tipo es similar a la de los constructores normales, sólo que ahora la definición ha de ir prefijada del modificador static y no puede contar con parámetros ni inicializador. O sea, se define de la siguiente manera:


static <nombreTipo>()
{
 <código>
}

   
En la especificación de C# no se ha recogido cuál ha de ser el orden exacto de las llamadas a los constructores de tipos cuando se combinan con herencia, aunque lo que sí se indica es que se ha de asegurar de que no se accede a un campo estático sin haberse ejecutado antes su constructor de tipo. Todo esto puede verse más claro con un ejemplo:


using System;
class A
{
 public static X;
 static A()
 {
  Console.WriteLine("Constructor de A");
  X=1;
 }
}
class B:A
{
 static B()
 {
  Console.WriteLine("Constructor de B");
  X=2;
 }
 
 public static void Main()
 {
  B b = new B();
  Console.WriteLine(B.X);
 }
}

    La salida que muestra por pantalla la ejecución de este programa es la siguiente:


Inicializada clase B

Inicializada clase A

2

    En principio la salida de este programa puede resultar confusa debido a que los primeros dos mensajes parecen dar la sensación de que la creación del objeto b provocó que se ejecutase el constructor de la clase hija antes que al de la clase padre, pero el último mensaje se corresponde con una ejecución en el orden opuesto. Pues bien, lo que ha ocurrido es lo siguiente: como el orden de llamada a constructores de tipo no está establecido, el compilador de Microsoft ha llamado antes al de la clase hija y por ello el primer mensaje mostrado es Inicializada clase B. Sin embargo, cuando en este constructor se va a acceder al campo X se detecta que la clase donde se definió aún no está inicializada y entonces se llama a su constructor de tipo, lo que hace que se muestre el mensaje Incializada clase A. Tras esta llamada se machaca el valor que el constructor de A dió a X (valor 1) por el valor que el constructor de B le da (valor 2) Finalmente, el último WriteLine() muestra un 2, que es el último valor escrito en X.

Constructores
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:13/10/2006
Última actualizacion:13/10/2006
Visitas totales:44500
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com