|
Introducción rápida al Object Pascal
Copyright © 2000-2002 Ernesto De Spirito. Este
artículo fué publicado en el Boletín Pascal.
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.
|