Fecha de revisión: 25/Feb/2005

Freya debe adaptarse a la plataforma sobre la que se ejecuta para lograr el mejor rendimiento posible. Este no es un asunto menor: aunque teóricamente Visual Basic.NET y C# son lenguajes "equivalentes", el código generado por el primero no es tan eficiente como el código generado por el segundo. Y gran parte de la culpa la tienen algunas manías de Visual Basic que afectan a la calidad de la traducción a IL.

LOS NUEVE MILLONES DE NOMBRES DEL CONSTRUCTOR

Una de las características de Delphi que perjudican la eficiencia es definición de constructores con nombres. Por ejemplo, la clase TCustomForm de Delphi declara estos dos constructores:

    // Esto es Delphi
    constructor Create(AOwner: TComponent);
    constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0);

Obviaremos de momento el hecho de que ambos son constructores virtuales. El constructor básico es, en realidad, el segundo. Create realiza el mismo trabajo que CreateNew llamando explícitamente a éste, y luego inicializa el formulario leyendo un recurso desde el ejecutable o DLL al que pertenece. El programador que quiere crear formularios en Delphi debe indicar cuál de los dos constructores utilizará para inicializar la nueva instancia:

    // Esto también es Delphi
    var
        F1, F2: TForm;
    begin
        F1 := TForm.Create(nil);
        F2 := TForm.CreateNew(nil);
        ...
    end;

Otros lenguajes no permiten dar nombres diferentes a los constructores de una clase, y el ejemplo más señalado es C++. Volvamos a la declaración de CreateNew:

    // Esto sigue siendo Delphi
    constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0);

¿Por qué hay un parámetro Dummy, con un valor por omisión? La culpa la tiene C++ Builder. La VCL debe ser compatible con este entorno de desarrollo. Como C++ no permite constructores con nombres, la única forma mediante la cual podríamos tener dos constructores en este lenguaje es diferenciándolos por sus respectivas listas de parámetros. Desde C++, los constructores de TCustomForm tendrían estas declaraciones simplificadas, en la que hemos quitado el atributo virtual y el convenio de llamada:

    // Esto es C++ Builder
    TCustomForm(TComponent* AOwner);
    TCustomForm(TComponent* AOwner, int Dummy);

Cada vez que el programador llama a CreateNew en Delphi, el compilador debe añadir instrucciones para pasar un parámetro entero al constructor. Esto significa espacio malgastado en el segmento de código, y tiempo de ejecución desperdiciado en una operación inútil.

El Common Language Runtime, o CLR, tampoco permite definir constructores con nombres. Todos los constructores en IL, el lenguaje intermedio, se llaman .ctor, con el punto delante, o .cctor, si se trata de un constructor de clase, o inicializador de tipo. Si Freya permitiese constructores con nombres, como hace Delphi.NET para mantener la compatibilidad con versiones anteriores, tendría que emplear parámetros ocultos para eliminar cualquier ambigüedad que esta característica pudiese introducir. ¿Hay alguna ventaja importante relacionada con los constructores con nombres, por la que merezca la pena pagar el precio en eficiencia? Sinceramente, no veo ninguna. Por lo tanto, los constructores de Freya siempre reciben el mismo nombre que la clase donde se han declarado, como en Java, C++ y C#:

    // Freya
    Stack = class[X]    // Una clase genérica
    public
        constructor Stack(Capacity: Integer);
        constructor Stack;
        ...
    end;

Podemos definir varios constructores siempre que tengan listas de parámetros diferentes, como muestra el ejemplo anterior.

IMPLEMENTANDO EL CONSTRUCTOR

C# y Java son lenguajes que no separan las declaraciones de una clase de su implementación, pero C++ se parece más a Delphi en este sentido. En C++, si implementamos el constructor fuera de la declaración de la clase, tendríamos que teclear algo parecido a lo siguiente:

// Esto es C++
template <class X>
Stack<X>::Stack(int aCapacity) {
    ...
}

Algo parecido ocurriría en Delphi. Sin embargo, debemos recordar que C++ y Delphi son lenguajes híbridos, que permiten tanto métodos como procedimientos globales, y es por este motivo que necesitan incluir el prefijo con el nombre de la clase. Freya es un lenguaje puro, y cuando se implementan constructores, o cualquier otro recurso que tenga el mismo nombre de la clase, no hay que repetir innecesariamente el nombre de la clase:

// Freya
implementation for Stack[X] is

    constructor Stack(Capacity: Integer);
    // No Stack[X].Stack, como sucedería en Delphi.
    begin
        // ... más adelante ...
    end;

Aquí estamos observando una característica importante de Freya: las implementaciones de los métodos de una clase se agrupan en una sección implementation for. Como consecuencia, no se repite el nombre de la clase al implementar un método, porque queda claro por el contexto.

El segundo constructor de la clase no tiene parámetros, porque se supone que creará una pila con una capacidad predeterminada. Al igual que en C#, Delphi y la gran mayoría de los lenguajes orientados a objetos, este segundo constructor puede implementarse llamando a otro constructor de la misma clase, y realizando cualquier otra inicialización necesaria a continuación:

    // Freya, otra vez
    constructor Stack[X]: Self(128);
    begin
    end;

Lo significativo en este constructor es la forma en que hemos ejecutado el código de la versión del constructor que tiene un solo parámetro:

constructor Stack: Self(128);

La sintaxis está inspirada en los lenguajes de la familia de C++, y soy consciente de que muchos programadores de Delphi fruncirán el ceño ante este ejemplo. Pero poco se puede hacer: una vez que hemos aceptado que los constructores no puedan distinguirse entre ellos por sus nombres, hay una larga lista de consecuencias que debemos aceptar. Si el constructor no tiene nombre, ¿cómo podríamos ejecutarlo desde el cuerpo de otro constructor como si se tratase de un método "normal"? Con la técnica propuesta perdemos la posibilidad de llamar al constructor en cualquier otra posición que no sea la primera instrucción, pero hay muy pocos casos en los que esta posibilidad sería realmente útil.

Una sintaxis parecida se utiliza para llamar, cuando es necesario, el constructor heredado por la clase:

// Freya
implementation for TForm is

    constructor TForm(AOwner: TComponent):
        Inherited(AOwner);
    begin
    end;

En vez de tener una palabra clave adicional, como el base de C# y Java, Freya utiliza un identificador Inherited, que permite tratar a Self como una instancia del ancestro inmediato. Inherited, por lo tanto, entra en la misma categoría que Self y Result, y en el caso de aserciones sobre el acceso para escritura de una propiedad, el identificador Value. Inherited también se emplea para llamar a métodos heredados desde otros métodos:

// Freya
implementation for ClaseDerivada is

    procedure MetodoVirtual(I: Integer);
    begin
        HacemosAlgo;
        if I > 0 then
            Inherited.MetodoVirtual(I - 1);
        HacemosOtraCosa;
    end;

INSTANCIACION

La eliminación de los constructores con nombres tiene importantes consecuencias en la forma en que se crean o instancian los objetos. Ya no podemos simular que aplicamos el constructor sobre el nombre de la clase, sino que tenemos que recurrir a una sintaxis similar a la de C#, C++ y Java. En vez de:

        // Delphi
        Button1 := TButton.Create(Self);

ahora debemos teclear:

        // Freya
        Button1 := new TButton(Self);

El constructor de la pila se implementaría de la siguiente manera:

// Freya
    constructor Stack(Capacity: Integer);
    begin
        Items := new Array[X](Capacity);
        // Array es el nombre de una clase genérica
    end;

Si le parece extraño este uso de new, tenga presente que estamos "recuperando" un recurso olvidado. El Pascal clásico utilizaba el procedimiento New para crear objetos en la memoria dinámica. Es cierto que lo hemos transformado en un operador, añadiendo una palabra reservada, pero tenga en cuenta que en Freya, como lenguaje orientado a objetos puro que es, un procedimiento "global" sentaría como una pamela sobre la cabeza de Atila el Huno.

NOTA
Alégrese pensando que ahora tendrá que escribir tres caracteres menos en cada instrucción de creación de instancias.

CONSTRUCTORES VIRTUALES

Freya conserva, al igual que lo hace Delphi.NET, los constructores virtuales tan característicos de Delphi. Mi experiencia como profesor me dice, sin embargo, que son muchos los programadores de Delphi, incluso algunos muy curtidos, que no tienen muy claro qué hacen estos constructores y por qué son útiles. Para empezar, ¿qué quiere decir que un constructor es virtual?

Le propondré una analogía con el mecanismo de llamada de métodos virtuales "normales". Cuando se ejecuta una llamada a un método virtual en Delphi, no es posible saber en tiempo de compilación cuál versión concreta del método es la que se ejecutará... si es que se trata de un método que ha sido redefinido en clases derivadas. La técnica utilizada para realizar la llamada correcta en tiempo de ejecución se conoce en inglés como late binding, o enlace tardío, y conservaré el nombre en inglés para esta breve explicación.

Lo que nos importa ahora es que, para poder ejecutar un método virtual de modo que se active el late binding, es necesario aplicar el método a una referencia a objeto. En los lenguajes modernos como Freya, siempre que manejamos un objeto es porque tenemos precisamente una referencia a él. En cambio, en un lenguaje híbrido como C++, podemos alojar objetos tanto en la memoria dinámica como en la pila. El late binding sólo tendría lugar en el primer caso:

// C++
class MiClase {
public:
    virtual void Metodo() {}
};

void Prueba() {
    MiClase obj1;
    obj1.Metodo();    // No hay late binding

    MiClase* obj2 = new MiClase();
    obj2->Metodo();   // Hay late binding
}

En el primer caso del ejemplo anterior, no hay late binding porque no hay ambigüedad alguna: la variable obj se refiere muy claramente a un objeto local de clase MiClase. En cambio, el puntero obj2 puede apuntar indistintamente a una instancia de MiClase, como en el ejemplo, o a una instancia de cualquier clase derivada de MiClase. Es cierto que en este ejemplo es muy sencillo descubrir que obj2 apunta realmente a una instancia de MiClase, pero en el caso general esto es imposible de determinar.

NOTA
Sin embargo, estos casos triviales son muy frecuentes en la práctica: creamos un objeto con un tiempo de vida asociado al método activo, y realizamos una llamada a un método que puede ser virtual. El compilador de Freya deberá detectar estos casos simples, en los que no hay ambigüedad en el tipo de instancia asociado a la variable, para optimizar la llamada y eliminar el late binding.

En lenguajes como C#, C++ y Java, el operador new siempre se aplica sobre el nombre de una clase, y por lo tanto, no hay ambigüedad posible sobre cuál constructor queremos que se ejecute. Sin embargo, Delphi y Freya permiten declarar variables pertenecientes a un tipo especial conocido como referencia de clase. Por ejemplo, esta es una referencia de clase muy conocida en Delphi:

// Delphi
type
    TComponentClass = class of TComponent;

La correspondiente declaración en Freya sería similar (excepto que la palabra clave type no se utiliza). Una variable de este nuevo tipo puede almacenar el nombre de cualquier clase derivada de TComponent, incluyendo a la misma TComponent. Veamos ahora dos formas diferentes de crear un botón en Delphi:

// Delphi
var
    C: TComponent;
    CC: TComponentClass; // TComponentClass = class of TComponent
begin
    C := TButton.Create(nil);
    ...
    CC := TButton;
    C := CC.Create(nil);
end;

En la primera de ellas, el constructor se aplica directamente sobre TButton, que a todos los efectos se comporta como una constante de clase. No hay ambigüedad en esa construcción. La segunda vía comienza por asignar TButton en una variable declarada como referencia de clase, y ejecutar el constructor sobre esa referencia. En este caso, es evidente que la variable CC contiene una referencia a la clase TButton, pero en el caso más general podría también hacer referencia a la clase TEdit o TDataSource. Si el constructor Create hubiese sido declarado virtual, como realmente sucede, la última instrucción debe recurrir a un mecanismo similar al late binding para saber cuál constructor concreto es el que debe ejecutar.

El mismo ejemplo se escribiría en Freya de esta manera:

// Freya
var
    C: TComponent;
    CC: TComponentClass;
begin
    C := new TButton(nil);
    ...
    CC := TButton;
    C := new CC(nil);
end;

El late binding se activa cuando el operador new se aplica sobre una variable cuyo tipo es una referencia de clase, en vez de aplicarse sobre un nombre de clase literal. Para declarar un constructor virtual en Freya sólo tenemos que añadir el correspondiente modificador:

// Freya
Component = class(Persistent)
public
    constructor Component(AOwner: Component); virtual;
    ...
end;

Dejaré para otro artículo la explicación de los detalles relacionados con la sintaxis, semántica e implementación de las referencias a clases, métodos de clase y constructores virtuales.

CONSTRUCTORES ESTATICOS

Finalmente, el Common Language Runtime permite asociar constructores de clase, o constructores estáticos, a una clase. Un constructor estático no puede tener parámetros asociados, por lo que sólo puede haber uno por cada clase. Su principal objetivo debe ser la inicialización de los campos estáticos de la clase, y no hay garantía sobre el momento en que se ejecutará... excepto que será ejecutado antes de que accedamos a cualquier otro miembro de la clase.

Como un constructor estático es un simple detalle de la implementación de la clase, su prototipo no se incluye en la declaración de la misma. En definitiva, ¡siempre tendrá el mismo prototipo! Si necesita un constructor estático, declárelo directamente en la sección implementation for de la clase:

// Freya
public

    MiClase = class
    public
        ... aquí, nada...
    end;

implementation for MiClase is

    class constructor MiClase;
    begin
        ...
    end;

end.

Como puede imaginar, el constructor de clase nunca es virtual.


Vea también:

Iteradores en Freya Propiedades en Freya
Métodos en Freya Excepciones en Freya
Interfaces en Freya