Una introducción rápida a la sintaxis del Object Pascal, el lenguaje de Delphi, destinada a programadores que no estén familiarizados con este lenguaje, y que traigan algún conocimiento de C/C++ y Java.

Introducción rápida al Object Pascal

Copyright © 2000-2002 Ernesto De Spirito.
Este artículo fué publicado en el Boletín Pascal.

Storage Library - Guarde la configuración de su aplicación

Este artículo no le enseñará Pascal ni Programación Orientada a Objetos (POO), y no es tampoco una referencia completa, sino que simplemente, como reza su título, es apenas una introducción rápida a la sintaxis del Object Pascal para aquellos programadores que no están familiarizados con este lenguaje. Asumimos que el lector tiene experiencia de programación en algún otro lenguaje de POO como C++ o Java y que ya entiende conceptos básicos como encapsulamiento, herencia y polimorfismo.

Notas generales

  • Pascal usa "begin" y "end" para encerrar sentencias en bloques, en lugar de llaves ("{}") como C o Java.
  • Las llaves ("{}") encierran comentarios. Se pueden usar "(*" y "*)" en su lugar (¡sin las comillas!). También se puede comentar hasta el final de la línea con "//" como en C o Java.
  • El punto y coma (";") es un separador de sentencias, no un terminador de las mismas como en C, de modo que no es necesario antes de un "end".
  • Se puede dividir una sentencia en muchas líneas como en C o Java dado que el retorno de carro no es un separador (el punto y coma lo es).
  • Los comentarios que comienzan con "$" son directivas del compilador.
  • Los valores numéricos hexadecimales se preceden por un "$", así como en C se precenden por "0x".
  • Un "#" precediendo un valor numérico denota un valor de tipo caracter, siendo ese caracter el correspondiente a ese valor numérico.
  • Los caracteres son compatibles en asignación con las cadenas: son tratados como cadenas de un caracter.
  • Las variables no son automáticamente inicializadas.
  • Las sentencias de asignación usan ":=", mientras que "=" es un operador de a comparación (en C y Java, "=" es el operador de asignación e "==" es el operador de comparación).
  • La sintaxis tipo C  a := b := c := 0;  no se admite.
  • Los operadores And, Or y Not tienen una precedencia más alta que los operadores de comparación, así que las operaciones de comparación se suelen encerrar entre paréntesis (a menos que use And, Or o Not como operadores de bits en vez de operadores booleanos). Por ejemplo, a > b and a > c se interpetaría como a > (b and a) > c, lo que generará un error de tipos. La expresión debería ser (a > b) and (a > c).
  • Los identificadores no distinguen mayúsculas de minúsculas.
  • Las cadenas y los caracteres se delimitan por comillas simples.

Estructura del código fuente de una aplicación

Normalmente una aplicación se compone de un fichero de programa (".dpr") y muchos ficheros de "unidades" (".pas"). Estas unidades son módulos como los ficheros ".c".

Un fichero de un programa mínimo comienza con la palabra "program" seguida del nombre interno del programa, y un bloque begin...end. Por ejemplo:

   program programa1;
   begin
     WriteLn('Hola mundo!');
   end.

WriteLn es un procedimiento que escribe una cadena en el dispositivo estándar de salida del sistema (normalmente la consola).

Un programa puede declarar constantes, tipos, variables, procedimientos y funciones. El siguiente programa no haca nada útil, pero sirve como ejemplo:

  program programa2;

  const
    MIN: integer = 1;
    MAX: integer = 20;

  type
    RangoValores = 1..20;
    ArregloValores = array[RangoValores] of integer;

  var
    Valor: integer;
    Valores: ArregloValores;

  function EstaEnRango(n: integer): boolean;
  begin
    if (n >= MIN) and (n <= MAX) then
      Result := True
    else
      EstaEnRango := False;
  end;


  procedure IngresarValores();

  procedure InicializarValores();
  var i: RangoValores;
  begin
    for i := MIN to MAX do Valores[i] := 0;
  end;

  begin
    InicializarValores;
    repeat
      WriteLn('Ingrese un valor entre 1 y 20 (0 p/salir): ');
      ReadLn(valor);
      if Valor <> 0 then
        if EstaEnRange(Valor) then
          Inc(Valores[Valor])
        else
          WriteLn('Valor inválido. Por favor intente de nuevo');
    until Valor = 0;
  end;


  function MaxValor(): RangoValores;
  var
    i: RangoValores;
  begin
    Result := MIN;
    for i := MIN + 1 to MAX do
      if Valores[i] > Valores[Result] then
        Result := i;
  end;

  begin
    IngresarValores;
    WriteLn('El valor más ingresado es: ', MaxValor());
  end.

La palabra "const" precede la declaración de constantes. En el ejemplo se declaran dos constantes enteras llamadas "MIN" y "MAX", representando los valores 1 y 20 respectivamente.

La palabra "type" precede la declaración de tipos definidos por el usuario. En el ejemplo declaramos un rango de valores enteros y un arreglo de 20 enteros indexados del 1 al 20.

La palabra "var" precede la declaración de variables. En el ejemplo declaramos una variable entera y un arreglo (de 20 enteros indexados del 1 al 20).

Las declaraciones de funciones se preceden por la palabra "function". Las funciones tienen un nombre, parámetros (opcionales) y un tipo de retorno. Por ejemplo, la función "EstaEnRango" toma un argumento entero y devuelve un valor booleano. Para devolver un valor, éste debe asignarse a la variable implícita "Result" o a la variable implícita llamada igual que la función de modo que "Result := ..." o "EstaEnRango := ..." tienen el mismo efecto.

La declaración de procedimientos se precede por la palabra "procedure" en vez de "function", y no tienen tipo de retorno dado que los procedimientos no devuelven valores.

Para llamar un procedimiento o una función, simplemente escriba su nombre (seguido de los parámetros reales encerrados entre paréntesis si es que el procedimiento o función toma parámetros). Por ejemplo, "EstaEnRango(Valor)" llama a la función EstaEnRango pasando la variable "Valor" como el argumento. "IngresarValores" en el bloque principal llama al procedimiento IngresarValores. "Inc(Valores[Valor])" (en C o Java se escribiría Valores[Valor]++ en su lugar) llama al procedimiento incorporado que incrementa su argumento. En realidad no se trata de un procedimiento dado que el compilador generará el código de máquina INC...

Los procedimientos y funciones pueden tener constantes, tipos y variables locales, así como también otros procedimientos y funciones. Se declaran igual que sus contrapartes a nivel de programa, excepto que las declaraciones se ponen entre la cabecera del procedimiento o función y el "begin" de su bloque de código. Por ejemplo la función "MaxValor" declara una variable local "i" y el procedimiento "IngresarValores" declara un procedimiento local "InicializarValores" que a su vez declara una variable local "i".

La ventaja de las funciones y procedimientos locales es que pueden acceder las variables locales y los parámetros de los procedimientos y/o funciones que los contienen, de modo que no se necesita pasarlos como argumentos, permitiendo un código más claro y eficiente.

Los procedimientos usados hasta ahora (ReadLn, que lee de la entrada estándar, WriteLn e Inc) son declarados en la unidad System que es "incluida" por el compilador de modo predeterminado, de manera tal que no necesitamos decirle que lo haga. La unidad System contiene muchas constantes, tipos, variables, procedimientos y funciones útiles, pero hay muchas más disponibles en otras unidades que vienen con Delphi. Por ejemplo, la mayoría de los elementos para trabajar con ficheros vienen en la unidad SysUtils y la mayoría de las API de Windows vienen en la unidad Windows. Para "incluir" estas unidades en un programa empleamos la cláusula "uses". Por ejemplo:

  program programa3;

  uses
    SysUtils, Windows;

  ...

Y por supuesto, podemos crear nuestras propias unidades. Las unidades comienzan con la palabra "unit" en vez de "program" y tienen cuatro secciones:

  • interface
    La sección interfaz contiene todas las declaraciones públicas (es decir, constantes, tipos, variables, procedimientos y funciones que estarán disponibles a programas y otras unidades)
  • implementation
    La sección implementación contiene todas las declaraciones a nivel de módulo (unidad) y la definición de los procedimientos y funciones declaradas en la sección de interfaz.
  • initialization
    Código para inicializar las variables públicas y modulares.
  • finalization
    Código para liberar recursos asignados por la unidad.

Por ejemplo:

  unit Unidad1;

  interface

  var globalvar: integer;

  procedure IncGlobalVar;

  implementation

  procedure IncGlobalVar;
  begin
    Inc(globalvar);
  end;

  initialization
    globalvar := 10;
  end.

Esta unidad declara una variable pública llamada globalvar y un procedimiento público llamado IncGlobalVar. En la sección de implementación este procedimiento es definido y en la sección de inicialización se establece el valor inicial de la variable. No hay sección de finalización porque no hay recursos que liberar.

Este programa usa la unidad declarada arriba:

  program programa4;
  uses
    Unidad1 in 'Unidad1.pas';

  begin // La unidad se inicializa aquí
    WriteLn(globalvar);  // 10
    IncGlobalVar;
    WriteLn(globalvar);  // 11
  end.

Sentencias de control de flujo

La sentencia "if"

  if <condición> then <sentencia>;

  if <condición> then
    <sentencia1>
  else
    <sentencia2>;

  if <condición> then begin
    <sentencia1>;
    <sentencia2>;
    <sentencia3>[;]
  end else
    <sentencia4>;

  if <condición> then begin
    <sentencia1>;
    <sentencia2>;
    <sentencia3>[;]
  end else begin
    <sentencia4>;
    <sentencia5>[;]
  end;

Note que no hay punto y coma antes de un "else". Necesita usar "begin...end" cuando las sentencias dentro de la estructura son más de una. <condición> es una expresión booleana (debe evaluarse como True o False).

La sentencia "case"

  case <selector> of
  <listacaso1>: <sentencia1>;
  ...
  <listacasoN>: <sentenciaN>;
  [else
    <sentencia>;]
  end;

<selector> debe ser una expresión ordinal (no puede ser una cadena por ejemplo). Las listas de casos son listas de valores separados por comas. Listas de casos válidas son por ejemplo:

  1:
  5..10:
  20, 30..40, 50..60, 67:

Cuando se necesita más de una sentencia en un case se deben encerrar mediante un bloque begin...end.

La sentencia "for"

  for <variable> := <ini> to/downto <fin> do <sentencia>;

Si usa "to" el incremento (o "step") es 1, y si usa "downto", el incremento es -1. Si necesita más sentencias dentro del bucle for debe encerrarlas en un bloque begin...end. Tenga cuidado: a la salida de un bucle for, el valor de la variable de control queda indefinido!

La sentencia "while"

  while <condición> do <sentencia>;

<condición> es una expresión booleana y si necesita más sentencias dentro del bucle while, ya sabe: begin...end.

La sentencia "repeat...until"

  repeat
    <sentencia1>;
    <sentencia2>;
    ...
    <sentenciaN>[;]
  until <condición>;

Las sentencias se repiten mientras la condición NO es satisfecha. La condición es evaluada al final, así que las sentencias de adentro se ejecutan al menos una vez. No se necesita begin...end.

La sentencia "goto"

Su uso se desaconseja en la programación estructurada. Si embargo es útil para salir de ciclos (bucles). Antes de usar goto, primero debe declarar una etiqueta con label y preceder la sentencia donde desea saltar por esa etiqueta seguida de dos puntos (":"), como en el siguiente ejemplo:

  function EstaCaracterEnCadena(char c, string s): boolean;
  var i, n: Integer;
  label CaracterEncontrado;
  begin
    n := Length(s);
    for i := 1 to n do
      if s[i] = c then goto CaracterEncontrado;
    Result := False;
    Exit; // Sale de un procedimiento o función
  CaracterEncontrado:
    Result := True;
  end;

Nunca use goto para saltar hacia adentro de un bucle u otra sentencia estructurada debido a que puede producir efectos impredecibles. No se permiten saltos de dentro hacia afuera, o de afuera hacia adentro de bloques try...except o try...finally. Tampoco se permiten saltos de y hacia otros procedimientos y funciones.

Siempre intente usar una alternativa estructurada, o por lo menos use el procedimiento Break:

  function EstaCaracterEnCadena(char c, string s): boolean;
  var i, n: Integer;
  begin
    Result := False;
    n := Length(s);
    for i := 1 to n do
      if s[i] = c then begin
        Result := True;
        break;  // Sale del bucle más interno
      end;
  end;

Tipos básicos

Los tipos básicos de Object Pascal se pueden clasificar de la siguiente forma:

  simples
    ordinales
      enteros
        Shortint    -128..127                 8 bits con signo
        Smallint    -32768..32767             16 bits con signo
        Longint     -2147483648..2147483647   32 bits con signo
        Integer     -2147483648..2147483647   32 bits con signo
        Int64       -2^63..2^63-1             64 bits con signo
        Byte        0..255                    8 bits sin signo
        Word        0..65535                  16 bits sin signo
        Longword    0..4294967295             32 bits sin signo
        Cardinal    0..4294967295             32 bits sin signo
      caracteres
        AnsiChar    1-bytes  caracter ANSI
        WideChar    2-bytes  caracter UNICODE
        Char        AnsiChar
      booleanos
        Boolean     1-byte (valor booleano de  8 bits)
        ByteBool    1-byte (valor booleano de  8 bits)
        WordBool    2-byte (valor booleano de 16 bits)
        LongBool    4-byte (valor booleano de 32 bits)
      enumeraciones
        type <tipo> = (valor1, ..., valorn);
        Ejemplo: type Palos = (Oro, Espada, Copa, Basto);
      subrangos
        type <tipo> = valor1..valor2;
        Ejemplo: type ExceptoOro = Espada..Basto;
                      Mayusculas = 'A'..'Z';
                      Horas = 0..23;
    reales
      Real48   2.9x10^-39..1.7x10^38      11-12 dígitos  6 bytes
      Single   1.5x10^-45..3.4x10^38        7-8 dígitos  4 bytes
      Double   5.0x10^-324..1.7x10^308    15-16 dígitos  8 bytes
      Extended 3.6x10^-4951..1.1x10^4932  19-20 dígitos 10 bytes
      Comp     -2^63+1..2^63 -1           19-20 dígitos  8 bytes
      Currency -922337203685477.5808
               .. 922337203685477.5807    19-20 dígitos  8 bytes
      Real     Double
  cadenas
    ShortString 255   caracteres   Compatibilidad hacia atrás
    AnsiString  ~2^31 caracteres   caracteres de 8 bits (ANSI)
    WideString  ~2^30 caracteres   Caracteres Unicode
    String      AnsiString
  estructurados
    conjuntos
      type <tipo> = set of <tipo-ordinal>;
    arreglos
      estáticos
        type <tipo> = array [<tipo-ordinal>] of <tipobase>;
        type <tipo> = array [<tipo-ordinal1>,
                          <tipo-ordinal2>,...] of <tipobase>;
      dinámicos
        type <tipo> = array of <tipobase>;
    registros
      type <tipo> = record
             <listacampos1>: <tipo1>;
             ...
             <listacamposN>: <tipoN>;
           end;
    ficheros
      type <tipo> = file of <tipobase>;
      type <tipo> = file;  // Fichero sin tipo
    clases
      // Las dejamos para el próximo newsletter
    referencias a clases
      type <tipo> = class of <tipo-class>;
    interfaces
      // Las dejamos para el próximo newsletter
  punteros
    type <tipo> = ^<tipobase>;
  procedimientos (punteros a procedimientos o funciones)
    type <tipo> = <declaración de procedimiento o función>;
    type <tipo> = procedure(<parámetros>) of object;
    Ejemplos: type TFuncionEntera = function: Integer;
                   TProcedure = procedure;
                   TStrProc = procedure(const S: string);
                   TMathFunc = function(X: Double): Double;
                   TMethod = procedure of object;
                   TNotifyEvent = procedure(Sender: TObject) of object;
  variantes
    Las variables tipo Variant pueden contener un valor de cualquier
    tipo, excepto los tipos estructurados, punteros e Int64. Sin embargo,
    sí pueden contener arreglos dinámicos y una tipo especial de arreglos
    estático llamado arreglo variante. También pueden contener objetos
    COM y CORBA.

Programación Orientada a Objetos

Las clases se declaran en las declaraciones de tipos (type) de un programa o en la sección de interfaz (interface) de una unidad. Una clase de compone de miembros que pueden ser campos de datos, métodos y propiedades.

Veámoslo con un ejemplo de una clase que llamaremos TRectangulo y que representa un rectángulo:

  type

    TRectangulo = class
      X1, Y1, X2, Y2: single;
      function getAncho: single;
      function getAltura: single;
      procedure setAncho(Ancho: single);
      procedure setAltura(Altura: single);
    end;

Esta clase tiene cuatro campos (X1, Y1, X2 y Y2) que representan las coordenadas de las esquinas del rectángulo, y cuatro métodos para obtener/establecer las dimensiones del mismo. En la sección de implementación (implementation) estos métodos se podrían definir como sigue:

  function TRectangulo.getAltura: single;
  begin  Result := Y2 - Y1;  end;

  function TRectangulo.getAncho: single;
  begin  Result := X2 - X1;  end;

  procedure TRectangulo.setAltura(Altura: single);
  begin  Y2 := Y1 + Altura;  end;

  procedure TRectangulo.setAncho(Ancho: single);
  begin  X2 := X1 + Ancho;  end;

Declarar objetos de una clase es igual a declarar variables de cualquier tipo. Por ejemplo:

  var
    r1, r2: TRectangulo;

Aquí hemos declarado dos objetos de la clase TRectangulo. En C++ uno está limitado a declarar objetos dentro de funciones. La memoria se asigna para esos objetos en la pila y sus constructores predeterminados son llamados al entrar a la función. Asimismo, los destructores son llamados automáticamente al volver de la función. Ése no es el caso en Object Pascal. Las variables objeto son referencias (como punteros, pero no se necesita el operador de derreferencia ^) así que usan espacio mínimo en el segmento de datos o en la pila, y uno debe explícitamente crear y liberar los objetos. Por ejemplo:

  r1 := TRectangulo.Create;   // Crea el objeto
  r1.X1 := 10;
  r1.Y1 := 35;
  r1.X2 := 50;
  r1.Y2 := 60;
  ShowMessage('Ancho = ' + FloatToStr(r1.getAncho) + #13
            + 'Altura = ' + FloatToStr(r1.getAltura));
  r1.Free;                   // Libera el objeto

Cuando crea un objeto, su memoria es asignada en el montículo y obtiene de vuelta una referencia que debería asignar a una variable objeto. Luego debería liberar la memoria llamando al método Free. Ahora bien, no declaramos Create ni Free, así que ¿de donde salieron? Son métodos de TObject y toda clase es descendiente de TObject, así que toda clase tiene disponibles esos métodos. Entonces,

    TRectangulo = class

y

    TRectangulo = class(TObject)

son la misma cosa: TRectangulo es un descendiente directo de TObject. Use esta sintaxis si desea que una clase herede de otra:

  type

    clase2 = class(clase1)
      <declaraciones de miembros>
    end;

Aquí clase2 hereda de clase1, o puede decirse que es una clase derivada, clase hija o subclase de clase1, o que clase1 es la clase base, clase madre o superclase de clase2.

Ahora, dijimos que además de campos y métodos, una clase podía tener propiedades. ¿Qué es una propiedad? Es una construcción conveniente para permitir un mayor nivel de ocultamiento. Para el programador que usa la clase, las propiedades se ven como si fueran campos, pero en realidad pueden ser campos o llamadas a métodos. Por ejemplo, agreguemos dos propiedades a nuestra clase TRectangulo:

  type

    TRectangulo = class
      ...
      property Ancho: single read getAncho write setAncho;
      property Altura: single read getAltura write setAltura;
    end;

De esta manera, un código como el siguiente:

  r1.Ancho := 20;
  r1.Altura := 10;
  ShowMessage('Ancho = ' + FloatToStr(r1.Ancho) + #13
            + 'Altura = ' + FloatToStr(r1.Altura));

sería automáticamente traducido por el compilador a:

 r1.Ancho(20);
  r1.setAltura(10);
  ShowMessage('Ancho = ' + FloatToStr(r1.getAncho) + #13
            + 'Altura = ' + FloatToStr(r1.getAltura));

Los miembros de una clase pueden ser privados, protegidos o públicos. Los miembros privados son sólo accesibles desde dentro de la unidad en la que se declara la clase, de modo que todas las clases dentro de una unidad son "amigas" en la jerga del C++. Un miembro protegido es como un miembro privado, excepto que puede también accederse desde descendientes de la clase aunque estén en unidades distintas. Los miembros públicos son siempre visibles. También puede haber miembros publicados que son miembros públicos para los que se genera información RTTI (RunTime Type Introspection: introspección de tipos en tiempo de ejecución) permitiendo a una aplicación consultar los campos y propiedades de un objeto para localizar sus métodos en tiempo de ejecución. Delphi usa RTTI para acceder a los valores de propiedades cuando guarda o carga archivos de formulario (.DFM), para mostrar propiedades en el Inspector de Objetos y para asociar métodos específicos (controladores o manejadores de eventos) con propiedades específicas (eventos) así que los miembros publicados son muy comunes al escribir componentes visuales.

Aquí va un ejemplo de una clase que usa todos estos tipos de miembros:

    {$TYPEINFO ON}
    TRectangulo = class
    private
      FX1, FY1, FX2, FY2: single;
    protected
      function getAncho: single;
      function getAltura: single;
      procedure setAncho(Ancho: single);
      procedure setAltura(Altura: single);
    public
      function getSuperficie: single;
    published
      property X1: single read FX1 write FX1;
      property Y1: single read FY1 write FY2;
      property X2: single read FX2 write FX2;
      property Y2: single read FY2 write FY2;
      property Ancho: single read getAncho write setAncho;
      property Altura: single read getAltura write setAltura;
      property Superficie: read getSuperficie;
    end;
    {$TYPEINFO OFF}

Usamos una directiva del compilador para activar y desactivar la RTTI. Como puede ver, la propiedad Superficie es de sólo lectura.

Una clase puede tener un constructor y un destructor:

  type

    TFile = class
    private
      FFile: File;
    public
      constructor Create(NomFich: string);
      destructor Destroy(); override;
    end;

  implementation

  constructor TFile.Create(NomFich: string);
  begin
    Assign(FFile, NomFich);
    Reset(FFile, 1);
  end;

  destructor TFile.Destroy;
  begin
    Close(FFile);
  end;

En este ejemplo, el constructor Create redefine al constructor Create predeterminado de TObject y toma como parámetro el nombre del fichero que será abierto. El destructor Destroy cierra el fichero. Debemos usar un destructor para liberar todos los recursos asignados por un objeto.

Una clase puede tener muchos constructores, por ejemplo sobrecargando el método Create o declarando nuevos constructores:

  type

    TFile = class
    private
      FFile: File;
    public
      constructor Create(NomFich: string); overload;
      constructor Create(NomFich: string;
                         Modo: integer); overload;
      constructor Crear(NomFich: string;
                        Tamanio: integer); overload;
      destructor Destroy(); override;
    end;

Para permitir el polimorfismo, un método puede ser declarado virtual o dinámico en una clase base y luego sobredeclarado si es necesario en las clases derivadas. Por ejemplo:

 interface
 type

   TFigura = class(TObject)
     procedure Mostrar; virtual;
   end;

   TCirculo = class(TFigura)
     procedure Mostrar; override;
   end;

   TCuadrado = class(TFigura)
     procedure Mostrar; override;
   end;

   TOvalo = class(TCirculo)
     procedure Mostrar; override;
   end;

  implementation

  procedure TFigura.Mostrar; begin ShowMessage('Figura'); end;

  procedure TCirculo.Mostrar; begin ShowMessage('Circulo'); end;

  procedure TCuadrado.Mostrar;begin ShowMessage('Cuadrado'); end;

  procedure TOvalo.Mostrar; begin ShowMessage('Ovalo'); end;

TFigura es la clase base de TCirculo y TCuadrado, y declara un método virtual (Mostrar) que es sobredeclarado (con override) en sus descendientes. El verdadero método Mostrar que será llamado dependerá del verdadero objeto al que una variable TFigura esté referenciando al momento de la llamada, así que se determina en tiempo de ejecución:

  var
    Figura: TFigura;
    Circulo: TCirculo;
    Cuadrado: TCuadrado;
    Ovalo: TOvalo;
  begin
    // Primero creamos los objetos
    Circulo := TCirculo.Create;
    Cuadrado := TCuadrado.Create;
    Oval := TOval.Create;

    Figura := Circulo;  // Figura ahora referencia un objeto TCirculo
    Figura.Mostrar;     // Llama a TCirculo.Mostrar
    Figura := Cuadrado; // Figura ahora referencia un objeto TCuadrado
    Figura.Mostrar;     // Llama a TCuadrado.Mostrar
    Figura := Ovalo;    // Figura ahora referencia un objeto TOvalo
    Figura.Mostrar;     // Llama a TOvalo.Mostrar

    // Libera los objetos
    Circulo.Free;  Cuadrado.Free;  Ovalo.Free;
  end;

Si Mostrar no se hubiera declarado como virtual, hubiese sido un método estático y por consiguiente en el ejemplo anterior TFigura.Mostrar hubiera sido llamado tres veces.

En vez de "virtual" se puede usar la palabra reservada "dynamic". La única diferencia es que virtual está optimizado para velocidad de ejecución mientras que dynamic está optimizado para reducir el tamaño del código, así que --por razones de rendimiento-- virtual es preferido para implementar polimorfismo mientras que dynamic se usa sólo en los raros casos en que una clase base tiene métodos virtuales que son ocasionalmente sobredefinidos por sus clases derivadas.

Clases abstractas

Delphi permite la definición de clases abstractas. ¿Qué es una clase abstracta? Una clase abstracta es una clase no pensada para ser instanciada sino subclasada, así que no puede crear objetos de esta clase, pero puede derivarla para crear clases descendientes, ya sean abstractas o no.

Recordemos que en el artículo precedente de esta serie definimos una clase polimórfica que tenía un método virtual sobredefinido por sus descendientes:

 interface
 type

 interface
 type

   TFigura = class(TObject)
     procedure Mostrar; virtual;
   end;

   TCirculo = class(TFigura)
     procedure Mostrar; override;
   end;

   TCuadrado = class(TFigura)
     procedure Mostrar; override;
   end;

   TOvalo = class(TCirculo)
     procedure Mostrar; override;
   end;

  implementation

  procedure TFigura.Mostrar; begin ShowMessage('Figura'); end;

  procedure TCirculo.Mostrar; begin ShowMessage('Circulo'); end;

  procedure TCuadrado.Mostrar;begin ShowMessage('Cuadrado'); end;

  procedure TOvalo.Mostrar; begin ShowMessage('Ovalo'); end;

Una variable de tipo TFigura, por ejemplo Figura, puede contener una referencia no sólo a un objeto TFigura, sino también a un descendiente de TFigura (como TCirculo, TCuadrado y TOvalo). Dado que el método Mostrar es virtual (no estático), cuando se llama a Figura.Mostrar, será ejecutado el método correspondiente a la clase del objeto que Figura referencia al momento de la llamada.

  var
    Figura: TFigura;
    Circulo: TCirculo;
    Cuadrado: TCuadrado;
    Ovalo: TOvalo;
  begin
    // Primero creamos los objetos
    Circulo := TCirculo.Create;
    Cuadrado := TCuadrado.Create;
    Oval := TOval.Create;

    Figura := Circulo;  // Figura ahora referencia un objeto TCirculo
    Figura.Mostrar;     // Llama a TCirculo.Mostrar
    Figura := Cuadrado; // Figura ahora referencia un objeto TCuadrado
    Figura.Mostrar;     // Llama a TCuadrado.Mostrar
    Figura := Ovalo;    // Figura ahora referencia un objeto TOvalo
    Figura.Mostrar;     // Llama a TOvalo.Mostrar

    // Libera los objetos
    Circulo.Free;  Cuadrado.Free;  Ovalo.Free;
  end;

Ahora bien, ¿Qué tiene que ver esto con las clases abstractas? Bueno, supongamos que el método Mostrar fuera tan específico de los descendientes de TFigura como para implementarlo en una clase tan general como lo es TFigura. Delphi permite la creación de una clase dejando algunos de sus métodos (virtuales o dinámicos) sin definir. Por ejemplo, podríamos declarar TFigura del siguiente modo:

    TFigura = class(TObject)
      procedure Mostrar; virtual; abstract;
    end;

Al declarar un método como abstracto, entonces la clase es abstracta y no puede ser instanciada, pero sí derivada:

   TCirculo = class(TFigura)
     procedure Mostrar; override;
   end;

   TCuadrado = class(TFigura)
     procedure Mostrar; override;
   end;

   TOvalo = class(TCirculo)
     procedure Mostrar; override;
   end;

  implementation

  procedure TCirculo.Mostrar; begin ShowMessage('Circulo'); end;

  procedure TCuadrado.Mostrar;begin ShowMessage('Cuadrado'); end;

  procedure TOvalo.Mostrar; begin ShowMessage('Ovalo'); end;

Nótese que esta vez no definimos TFigura.Mostrar ya que es un método abstracto. El ejemplo que mostramos arriba funcionará exactamente igual. Sólo noten que podemos declarar una variable del tipo TFigura

  var Figura: TFigura;

pero que no podemos crear objetos de esa clase:

    Figura := TFigura.Create;  // Produciría un error de compilación

El beneficio de las clases abstractas es que permiten estableces clases bases para polimorfismo sin tener que proveer implementaciones predeterminadas para métodos que generalmente son muy específicos de los descendientes como para ser conocidos de antemano al definir la clase. Por ejemplo puede definir una pila de cadenas de abstracta:

    TCustomStringStack = class
      procedure clear; virtual; abstract;
      procedure push(s: string); virtual; abstract;
      function pop: string; virtual; abstract;
      function top: string; virtual; abstract;
    end;

Los descendientes de TCustomStringStack tendrán por lo menos esos métodos y pueden implementar la pila de cualquier manera. Por ejemplo puede tener una pila basada en un arreglo (que la podríamos llamar TArrayStringStack), o en una lista enlazada (TListStringStack) o en un fichero en disco (TDiskStringStack), pero independientemente de la implementation (no conocida al momento de definir la clase base abstracta), usted sabrá que una variable de tipo TCustomStringStack puede manejar las operaciones básicas de todos estos tipos de pilas (TArrayStringStack, TListStringStack o TDiskStringStack) y que es la asignación es compatible con ellas.

La VCL está llena de clases abstractas (normalmente tienen la palabra "Custom" en sus nombres) y muchas propiedades que son objetos son del tipo de una clase abstracta.

Interfaces

Las interfaces son un poco como las clases, excepto que:

  • Son declaradas con la palabra 'interface' en lugar de 'class'.
  • Sus nombres generalmente comienzan con 'I' en vez de con 'T'.
  • No tienen constructores ni destructores.
  • No pueden ser instanciadas.
  • No pueden tener campos: sólo métodos y propiedades.
  • Como no tienen campos, los especificadores read y write de las propiedades sólo pueden ser métodos.
  • Los métodos no pueden ser declarados como virtuales, dinámicos, abstractos o sobredefinidos. Se podría decir que son abstractos.
  • La convención de llamada predeterminada es register. Use stdcall para interfaces compartidas entre módulos (especialmente si están escritas en diferentes lenguajes). Use safecall para implementar métodos de interfaces duales o interfaces CORBA.
  • No se permiten especificadores de visibilidad (public, private, protected y published). Todos los miembros son públicos.
  • Son descendientes de IUnknown (el equivalente de TObject para las interfaces).
  • Pueden tener un GUID (identificador global único) para identificar unívocamente una interface. Se usa al consultar una interface para obtener referencias a sus implementaciones. Un GUID es un valor de 16 bytes (vea el ejemplo).

Esta es una declaración de una interfaz:

  type
    IRotacion = interface(IUnknown)
      ['{01234567-89AB-CDEF-0123-456789ABCDEF}']
      procedure Rotar(grados: single); stdcall;
    end;

El propósito de una interfaz es eventualmente ser implementada por una clase. También puede servir como interfaz base para sus descendientes. Una clase puede implementar muchas interfaces. Por ejemplo, nuestra clase TFigura puede implementar la interfaz IRotacion que acabamos de declarar:

  type
    TFigura = class(TObject, IRotacion)
      procedure Mostrar; virtual, abstract override;
      procedure Rotar(grados: single); stdcall;
      function QueryInterface(const IID: TGUID; out Obj):
                              HResult; stdcall;
      function _AddRef: Integer; stdcall;
      function _Release: Integer; stdcall;
    end;

¿De dónde salieron todos esos métodos? Bueno, una clase que implementa interfaces debe declarar e implementar todos sus métodos. En este caso, todos los métodos de IRotacion deberían ser implementados. IRotacion tiene 4 métodos: Rotar y 3 métodos que hereda de IUnknown. Si no quisiéramos implementar esos métodos, podemos derivar de la clase TInterfacedObject (un descendiente directo de TObject) o de TComponent (un ancestro común de muchos componentes visuales y no visuales) dado que ya implementan esos tres métodos, de modo que para evitar tener que hacerlo nosotros mismos, la manera más fácil de implementar una interfaz es usando una clase derivada de una de esas dos clases o una de sus descendientes. Por ejemplo:

    TFigura = class(TInterfacedObject, IRotacion)
      procedure Mostrar; virtual; abstract;
      procedure Rotar(grados: single); stdcall;
    end;

De esta forma sólo nos tenemos que preocupar de Rotar. Un método implementado puede ser virtual y abstracto, así que si queremos podemos dejarle la implementación a los descendientes de TFigura:

  interface
  type
    TFigura = class(TInterfacedObject, IRotacion)
      procedure Mostrar; virtual; abstract;
      procedure Rotar(grados: single); virtual; stdcall; abstract;
    end;

    TCuadrado = class(TFigura)
      procedure Mostrar; override;
      procedure Rotar(grados: single); override; stdcall;
    end;

    TOvalo = class(TCuadrado)
      procedure Mostrar; override;
      procedure Rotar(grados: single); override; stdcall;
    end;

  implementation

  procedure TCuadrado.Mostrar; begin ShowMessage('Cuadrado'); end;
  procedure TCuadrado.Rotar(grados: single);
  begin ShowMessage('Cuadrado Rotado'); end;

  procedure TOvalo.Mostrar; begin ShowMessage('Ovalo'); end;
  procedure TOvalo.Rotar(grados: single);
  begin ShowMessage('Ovalo Rotado'); end;

Ahora podemos escribir un código como este:

  var
    Cuadrado: TCuadrado;
    Ovalo: TOvalo;
    Rotacion: IRotacion;
  begin
    Cuadrado := TCuadrado.Create;
    Ovalo := TOvalo.Create;

    Rotacion := Cuadrado;
    Rotacion.Rotar(45);  // Llama a TCuadrado.Rotar
    Rotacion := Ovalo;
    Rotacion.Rotar(90);  // Llama a TOvalo.Rotar
  end;

Como puede ver, el uso de interfaces es muy similar al polimorfismo. La diferencia es que es más abierto ya que a una variable de tipo interfaz le puede asignar un objeto de CUALQUIER clase con la condición que dicha clase implemente la interfaz, no con la condición que sea derivada de una cierta clase base. Por ejemplo, si tuviéramos otra clase no descendiente de TFigura que implementara IRotacion, como esta:

    TMiBoton = class(TComponent, IRotacion)
      procedure Rotar(grados: single); stdcall;
    end;

Entonces estas sentencias también serían válidas:

    MiBoton := TMiBoton.Create;

    Rotacion := MiBoton;
    Rotacion.Rotar(180);  // Llama a TMiBoton.Rotar

Hay un par de cosas importantes que se deben conocer acerca de las interfaces y de la implementación de los 3 métodos de IUnknown hecha por TInterfacedObject. Primero, los objetos cuentan las referencias, significando ésto que cuando realiza una asignación, el valor de la cuenta se incrementa (_AddRef es llamado implícitamente). Este valor se almacena en TInterfacedObject.FRefCount y después que crea un objeto se establece en cero. Por ejemplo, después de

    Cuadrado := TCuadrado.Create;

Cuadrado.FRefCount será cero. Después de una asignación como

    Rotacion := Cuadrado;

Cuadrado.FRefCount será 1. Cuando asigne otro objeto a Rotacion, como

    Rotacion := Ovalo;

la cuenta se decrementa (_Release es llamado implícitamente), y si se hace cero, el objeto es liberado. Eso explica por qué no liberamos Cuadrado en el ejemplo de más arriba. Tampoco liberamos Ovalo porque al salir de un procedimiento o función, a las variables locales de interfaz es como si se les asignara nil, así que la cuenta es decrementada alcanzando cero y entonces el objeto es liberado.

Algunas veces este comportamiento puede ser una molestia. ¿Qué sucedería si necesitáramos el objeto después de asignar la variable de interfaz a otro objeto? Bueno, tenemos dos opciones: alterar el valor de FRefCount o definir un nuevo objeto como TInterfacedObject pero implementando los métodos de IUnknown de una manera diferente.

Para la primera opción, el procedimiento del ejemplo anterior sería:

  var
    Cuadrado: TCuadrado;
    Ovalo: TOvalo;
    Rotacion: IRotacion;
  begin
    Cuadrado := TCuadrado.Create;
    Ovalo := TOvalo.Create;

    InterlockedIncrement(Cuadrado.FRefCount);
    Rotacion := Cuadrado;
    Rotacion.Rotar(45);  // Llama a TCuadrado.Rotar
    InterlockedIncrement(Ovalo.FRefCount);
    Rotacion := Ovalo;
    InterlockedDecrement(Cuadrado.FRefCount);
    Rotacion.Rotar(90);  // Llama a TOvalo.Rotar
    Rotacion := nil;
    InterlockedDecrement(Ovalo.FRefCount);

    Cuadrado.Free;
    Ovalo.Free;
  end;

Para la segunda opción, podríamos definir una clase TInterfacedObj:

  interface
  type
    TInterfacedObj = class(TObject, IUnknown)
    protected
      function QueryInterface(const IID: TGUID; out Obj): HResult;
                                                          stdcall;
      function _AddRef: Integer; stdcall;
      function _Release: Integer; stdcall;
    end;

  implementation

  function TInterfacedObj.QueryInterface(const IID: TGUID; out Obj):
    HResult;
  begin
    if GetInterface(IID, Obj) then
      Result := 0
    else
      Result := HResult($80004002); // E_NOINTERFACE
  end;

  function TInterfacedObj._AddRef: Integer; begin Result := 1; end;

  function TInterfacedObj._Release: Integer; begin Result := 1; end;

TFigura debería ser derivada de TInterfacedObj:

    TFigura = class(TInterfacedObj, IRotacion)
      ...

Y nuestro procedimiento de prueba sería como este:

  var
    Cuadrado: TCuadrado;
    Ovalo: TOvalo;
    Rotacion: IRotacion;
  begin
    Cuadrado := TCuadrado.Create;
    Ovalo := TOvalo.Create;

    Rotacion := Cuadrado;
    Rotacion.Rotar(45);  // Llama a TCuadrado.Rotar
    Rotacion := Ovalo;
    Rotacion.Rotar(90);  // Llama a TOvalo.Rotar

    Cuadrado.Free;
    Ovalo.Free;
  end;

Esta parece una forma más "natural" de programar.

JfControls Library - para Delphi y C++ Builder