Manual de FreePascal (parte final)
De Gleducar, http://www.gleducar.org.ar
Units
La necesidad de las units
Hasta ahora hemos empleado algunas funciones que se encontraban en algunas units estándar de FreePascal. Las units permiten reunir funciones, procedimientos, variables y tipos de datos para disponerlos de forma más ordenada y poderlos reutilizar en otras aplicaciones.
Una primera forma de reutilizar el código que hemos visto ha sido mediante la declaración de funciones. Estas funciones tenÃan que estar declaradas antes de que las hiciéramos servir. Habitualmente, pero, emplearemos funciones y procedimientos en varios programas y en otras units y no será práctico copiar de nuevo la implementación. Además, de esta forma tendrÃamos que cambiar muchas veces el código si lo optimizáramos o lo corrigiéramos, por ejemplo. Empleando units conseguimos reutilizar mejor las funciones, procedimientos pues estas se encontrarán en la unit y cualquier cambio que necesitemos sólo se tendrá que realizar en la unit misma. Cuando necesitemos llamar las funciones y los procedimientos de la unit, o emplear variables de ésta, sólo habrá que indicar al compilador que las emplee.
Utilizar una unit
Para emplear una unit en nuestro programa ya hemos visto que es tan simple como especificarla en la cláusula uses del programa. Normalmente necesitaremos más de una, pues las separamos entre comas.
uses Crt, Dos, SysUtils;
Hay que tener en cuenta de que la unit tendrá que ser visible para el compilador. Normalmente el compilador buscará el archivo que contenga la unit en el directorio del programa más algunos directorios del compilador. También podemos especificar la posición en el disco de la unit, en caso de que no se encontrara en el directorio actual, con el parámetro -Fudirectorio pasado al compilador, donde directorio es el directorio donde el compilador puede encontrar la unit. Otra forma es modificar el archivo PPC386.CFG al directorio C:\PP\BIN\WIN32 y añadir la lÃnea -Fudirectorio. Si tenemos que especificar más de un directorio pues añadiremos más lÃneas. De esta forma es permanente cada vez que se ejecuta el compilador. Finalmente podemos emplear la directiva de compilador, dentro del código del programa o unit, $UNITPATH separando los directorios con puntos y comas. :
{$UNITPATH directorio1;..;directorion}
Por ejemplo:
{$UNITPATH ..\graficos;c:\units}
Crear una unit
Para crear una unit hay que tener en cuenta que el archivo que vamos a crear, con extensión PAS o PP tiene que tener el mismo nombre que le daremos a la unit. Por ejemplo si nuestra unit se llama Utilidades (recuerde que las mayúsculas son poco importantes en Pascal) el archivo tendrá que ser UTILIDADES.PAS o bien UTILIDADES.PP. Es muy recomendable poner el nombre del archivo de la unit en minúsculas sobretodo en el caso de Linux donde los archivos son sensibles a mayúsculas y minúsculas.
El encabezado de una unit tiene que empezar con la palabra reservada unit y el nombre de la unit, que tiene que coincidir con el nombre del archivo. En nuestro caso serÃa :
unit Utilidades;
Esta palabra reservada indica al compilador que vamos a hacer una unit y no un programa. Además, a diferencia de program, que era opcional, unit es necesaria para indicarle al compilador que lo que se va a encontrar es una unit y no un programa, como suele ser por defecto.
Estructura de una unit
Una unit está formada por tres partes bien diferenciadas : una interfaz, una implementación y una sección de inicialización.
En la interfaz se declaran todas las variables, constantes, tipos y funciones o procedimientos que queremos que estén disponibles cuando vayamos a emplear la unit. En lo que los procedimientos y funciones concierne, los declararemos como si se trataran de funciones forward pero sin la palabra reservada forward.
En la implementación implementaremos las funciones de la interfaz y podemos declarar variables, tipos, constantes y otras funciones. A diferencia de los declarados en la interfaz, los elementos declarados en la implementación sólo están disponibles dentro de la misma unit y tienen como finalidad ser elementos auxiliares en la implementación.
Finalmente el código de inicialización se ejecutará al cargase la unit. Este código se ejecuta antes que se inicie el programa y tiene como finalidad realizar tareas de inicialización de la propia unit. Como es de esperar, la unit termina con un end.
La parte interface
La interfaz se implementa primero y se indica su inicio con la palabra reservada interface. Justo después de interface podemos especificar las units que empleará la función aunque sólo es recomendable hacerlo si alguno de los tipos de datos que vamos a emplear en interface se encuentra en otra unit. Si sólo la necesitamos para ciertas funciones ya la incluiremos dentro de la implementación. Esto es asà para evitar referencias circulares donde A emplea B y B emplea A, ya que el compilador emitirá un error. En la sección implementación este error no se puede dar.
Vamos a implementar diversas funciones y procedimientos simples en nuestra unit que nos faciliten un poco más el trabajo a la hora de pedir datos al usuario. Concretamente vamos a hacer diversas funciones sobrecargadas que emitirán un mensaje y comprobarán que el dato introducido es correcto. La unit empieza de la forma siguiente :
unit Utilidades; interface procedure PedirDato(Cadena : String; var Dato : String); procedure PedirDato(Cadena : String; var Dato : Integer); procedure PedirDato(Cadena : String; var Dato : Real);
Sólo lo implementaremos para estos tres tipos pues no se suelen pedir otros tipos de datos ni caracteres sueltos (en este caso pedirÃamos la pulsación de una tecla). Ahora sólo falta implementar las funciones en la sección de implementación.
La parte implementation
Una vez declarados en interface tipos de datos, constantes y variables no hay que hacer nada más. No es asà con las funciones y los procedimientos que hay que implementar. La sección de implementación empieza con la palabra reservada implementation. No importa mucho el orden en el que las implementemos sino que realmente las implementemos, de no hacerlo el compilador nos dará error.
implementation procedure PedirDato(Cadena : string; var Dada : string); begin Write(Cadena); Readln(Dato); end; procedure PedirDato(Cadena : string; var Dato : Integer); begin Write(Cadena); Readln(Dato); end; procedure PedirDato(Cadena : string; var Dato : Real); begin Write(Cadena); Readln(Dato); end; end. // Fin de la unit
Curiosamente las implementaciones de las tres funciones son idénticas pero como ya hemos comentado no tiene porque ser asÃ. Por ejemplo podÃamos haber pedido un string al usuario y haber añadido código de comprobación para ver si es un entero o no, devolviendo true o false en función de si la función ha tenido éxito. Esto es asà ya que Readln falla si al pedir un entero, o un real, el usuario introduce un dato que no es convertible a entero o real.
Inicialización de una unit
Supongamos que tenemos una unit que dispone de una orden llamada NotificarLog(Cadena : String); que se encarga de escribir en un archivo de logging (donde se anotará lo que va haciendo el programa y asà si falla saber dónde ha fallado) la cadena que se especifica como parámetro. En este caso nos interesa abrir un archivo de texto para hacer las anotaciones y cerrarlo si el programa termina ya sea de forma normal o anormal. Es por este motivo que las units incorporan una sección de inicialización y también de finalización. En la especificación habitual de Pascal sólo habÃa sección de inicialización pero recientemente a Delphi se le añadió la posibilidad de disponer también de una sección de finalización y FreePascal también la incorpora.
Inicialización "a la antigua"
Es posible inicializar, sólo inicializar, si antes del end. final añadimos un begin y en medio las sentencias de inicialización. Por ejemplo podrÃamos haber puesto unos créditos que se verÃan al inicializarse la unit.
begin
Writeln('Unit de utilidades 1.0 - Roger Ferrer Ibáñez');
end. // Fin de la unit
Este tipo de inicialización tiene la desventaja de que no permite la finalización de la unit de forma sencilla. Sólo nos será útil en algunos casos. Cuando necesitemos inicializar y finalizar tendremos que emplear otra sintaxis.
Inicialización y finalización
Debido a que la mayorÃa de inicializaciones requerirán una finalización es posible añadir secciones de inicialización y finalización en la parte final de la unit. Estas secciones irán precedidas por las palabras reservadas initialization y finalization. En este caso especial no es necesario rodear las sentencias de begin ni de end. Tampoco es obligatoria la presencia de los dos bloques : puede aparecer uno sólo o bien los dos a la vez.
Ahora ya podemos implementar nuestra función de logging.
unit Logging;
interface
procedure NotificarLog(Cadena : string);
implementation
var // Variable de la unit no accesible fuera de la unit
Log : Text;
procedure NotificarLog(Cadena : string);
begin
Writeln(Log, Cadena);
end;
initialization
Assign(Log, 'LOGGING.LOG');
Rewrite(Log);
finalization
Close(Log);
end. // Fin de la unit
Orden de inicialización y finalización
El orden de inicialización y finalización de las units depende exclusivamente de su posición en la cláusula uses. El orden es estrictamente el que aparece en esta cláusula. Supongamos que tenemos la cláusula siguiente en nuestro programa (o unit) :
uses Unit1, Unit2, Unit3;
Supongamos que todas tienen código de inicio y final. Entonces la Unit1 se inicializarÃa primera, después Unit2 y después Unit3.
El proceso de finalización es a la inversa. Primero se finalizarÃa la Unit3, después Unit2 y después Unit1. En general el proceso de inicio es simétrico : las units que se han inicializado primero también son las últimas a finalizarse.
En el caso de que una unit que empleamos necesite otra, entonces el criterio del compilador es inicializar todas las units que se encuentre más afuera primero y después las de más adentro. Por ejemplo, si en uses sólo hubiera Unit1 pero ésta empleara Unit2 y ésta última Unit3 entonces primero se inicializarÃa Unit3, después Unit2 y finalmente Unit1. El proceso de finalización seria el inverso, tal como se ha comentado.
A diferencia de C y de algunos lenguajes, incluir dos veces una misma unit en el código (por ejemplo que Unit1 y Unit2 empleen Unit3) no comporta ningún problema y la inicialización y finalización sólo se lleva a cabo una vez siguiendo el orden antes indicado.
Ã?mbito de una unit
Se llama ámbito a aquel conjunto de partes del código que tienen acceso a un identificador, ya sea una constante, variable, tipo o función/procedimiento.
Interface
Si el identificador se declara en la sección interface entonces el ámbito de este será :
- Toda la propia unit del identificador incluyendo las secciones interface, implementation, initialization y finalization.
- Todas las units y programas que tengan esta unit dentro de la cláusula uses.
Implementation
Si el identificador se declara en la sección implementation entonces su ámbito es :
- Toda la propia unit a excepción de la sección interface. Esto quiere decir, por ejemplo, que no podremos emplear tipos de datos declarados en implementation dentro de los parámetros de una función en interface, puesto que no serÃa posible para el programador pasar parámetros a esta función fuera de la propia unit pues no conocerÃa el tipo de dato.
- Fuera de la unit estos identificadores no son accesibles.
Incluir units dentro de interface
Es posible especificar una cláusula uses dentro de la sección interface. Esta posibilidad sólo se tendrÃa que emplear en caso de que algún identificador declarado en interface precise de algún otro tipo declarado en otra unit.
Situando la cláusula uses en interface permitimos que toda la unit, incluidas implementation, initialization y finalization, acceda a esta otra unit.
Ahora bien, esta posibilidad tiene el riesgo de que nuestra unit también se encuentre en la sección interface de la otra unit lo que resultarÃa en una referencia circular que es ilegal.
Incluir units dentro de implementation
En este caso sólo las secciones implementation, initialization, finalization tienen acceso a los identificadores declarados en la unit en uses. Es posible que dos units se incluyan mutuamente siempre que no se incluyan en interface a la vez, ya sea las dos en implementation o una de ellas en interface y la otra en implementation. Siempre que sea posible, es recomendable incluir las units en la cláusula de implementation.
LibrerÃas
LibrerÃas vs units
Hasta ahora hemos visto que todos los elementos que conformaban nuestro programa se resolvÃan en tiempo de compilación. Es lo que se llama enlace estático. Una vez se ha compilado el programa, el propio EXE dispone de todo lo necesario para ejecutarse.
En el caso de las units escritas compiladas con FreePascal, la compilación nos dará un archivo PPW que contiene la información que en tiempo de compilación se enlazará adecuadamente al EXE final.
Algunas veces, pero, el código no habrá sido generado por nosotros sino que con otros compiladores. Para poder emplear este código dentro de nuestro programa tendremos que enlazarlo. En algunos casos realizaremos enlaces estáticos, resueltos en tiempo de compilación, pero en otros tendremos que enlazar en tiempo de ejecución, especialmente en el caso de las librerÃas de enlace dinámico (DLL, Dynamic Library Linking) de Windows.
Archivos objeto
FreePascal permite el enlace estático con archivos llamados archivos objeto (que suelen llevar extensión O, o OW en Win32) que hayan sido compilados con los compiladores GNU Pascal, GNU C/C++ o algún otro compilador que compile este formato de archivos objeto. Los compiladores Borland, Microsoft e Intel generan un tipo de archivos objeto que en general no son directamente compatibles con los archivos objeto que vamos a emplear.
Importación de rutinas en archivos objeto
Supongamos que queremos enlazar una función muy sencilla escrita en C que tiene el código siguiente :
int incrementador(int a)
{
return a+1;
}
Esta función toma como parámetro un entero de 32 bits (el parámetro a) y devuelve otro entero que es el valor del parámetro incrementado en uno. En Pascal esta función serÃa la siguiente :
function incrementador(a : integer) : integer; begin incrementador := a+1; end;
Como se puede ver C y Pascal tienen un cierto parecido sintáctico. Supongamos que la rutina en C se encuentra almacenada en un archivo llamado incr.c. Ahora hay que compilarlo para obtener un archivo objeto. Como que no es un programa completo, pues no tiene bloque principal, indicaremos al compilador que sólo lo compile y no llame al enlazador, que nos darÃa un error. El compilador de C que emplearemos en los ejemplos en C es el conocido GNU CC, o gcc. Para compilar nuestro código ejecutamos el compilador desde la lÃnea de órdenes habitual:
gcc -c -o incr.o incr.c
El parámetro -c indica que sólo queremos compilación y no enlazado. El parámetro -o seguido de incr.o sirve para indicar que el archivo objeto que se creará llevará el nombre de incr.o. Finalmente incr.c es el archivo que estamos compilando.
Una vez compilada la rutina en un archivo objeto ahora hay que enlazarla en nuestro programa Pascal. Para hacerlo tendremos que realizar varios pasos.
Primero hay que indicar al compilador que habrá que enlazarse con el archivo objeto incr.o. Para hacerlo emplearemos la directiva de compilador $LINK seguida del nombre de archivo objeto a incluir. Aunque no es necesario incluir la extensión .o (que es la extensión por defecto) es recomendable.
{$LINK incr.o}
Una vez hemos incluido esta directiva en el principio del programa ahora habrá que declarar una función externa. Las funciones externas se declaran con la directiva external que indica al compilador que esta función se encuentra en un módulo externo al programa y que no la estamos implementando nosotros. En nuestro caso, además, tendremos que añadir la directiva cdecl pues esta rutina está escrita en C y conviene que el compilador pase los parámetros correctamente y además encuentre el nombre de la función dentro del archivo objeto. El archivo objeto no contiene los nombres de las funciones directamente sino que éste sufre modificaciones llamadas mangling. La directiva cdecl indica a FreePascal que busque el mangled name apropiado en C de nuestra función en el archivo objeto.
También hay que tener en cuenta que en este caso la declaración de la función tendrá en cuenta las mayúsculas y las minúsculas. En nuestro caso la función incrementador en C estaba toda en minúsculas y en el código Pascal vamos a tener que hacer lo mismo. Finalmente hay que emplear el mismo tipo de parámetro. En Pascal el tipo int de C es equivalente a Longint.
function incrementador(a : longint) : longint; cdecl; external;
Tal como hemos indicado, las siguientes posibles declaraciones serÃan sintácticamente correctas pero el enlazador no sabrÃa resolver a qué función estamos llamando.
function INCREMENTADOR(a : longint) : longint; cdecl; external; function incremENTADOR(a : longint) : longint; cdecl; external;
Una vez la función ha sido declarada ya la podemos emplear dentro de nuestro código ahora ya sin restricción de mayúsculas. Un programa de ejemplo de nuestro código serÃa el siguiente.
program FuncionC;
{$L INCR.O}
function incrementador(a : longint) : longint; cdecl; external;
var
numero : longint;
begin
write('Introduzca un número : '); Readln(numero);
Write('Número + 1 : ', Incrementador(numero));
end.
En realidad la mayorÃa de llamadas a funciones externas suelen ser más complejas pues pueden incorporar variables por referencia. En el ejemplo siguiente emplearemos un procedimiento que incrementa la variable que le pasemos. El código en C es el siguiente:
void incrementador(int* a)
{
*a = *a+1;
}
En realidad lo que estamos pasando es un puntero tal como indica el operador de desreferenciación de C *, similar al operador ^ de Pascal. Lo que hacemos es incrementar el contenido del puntero en una unidad. Esto es asà porque C no incorpora parámetros por referencia pero permite pasar punteros como parámetros. La directiva void indica que esta función no devuelve nada por lo que es un procedimiento. Una vez compilado y obtenido el archivo objeto nuestro programa puede quedar asÃ:
program FuncionC;
{$L INCR.O}
procedure incrementador(var a : Longint); cdecl; external;
var
numero : Longint;
begin
Write('Introduzca un número : '); Readln(numero);
Incrementador(numero);
Writeln('Número + 1 : ', numero);
end.
En realidad cuando pasamos un parámetro por referencia lo que estamos pasando es su puntero por tanto esta declaración asà es del todo correcta. Obsérvese que en este caso hemos declarado un procedimiento y no una función ya que, como sabemos, nuestro código no devuelve nada.
Aunque C no incorpora parámetros por referencia, C++ sÃ. Vamos a sustituir el código incr.c por un código en C++ que reciba los parámetros por referencia.
*void* incrementador(int &a)
{ a = a+1;
}
El operador & de C++ indica que el parámetro es por referencia. Como podemos ver ahora ya no hay que emplear el operador * ya que el parámetro se trata como una variable normal. Para compilar el código en C++ hay que emplear el compilador g++ en vez de gcc con los mismos parámetros que hemos empleado antes. Una vez obtenido el archivo objeto si intentamos compilar el archivo obtendremos un error. Esto es debido a que el mangling de C++ es muy distinto al de C. Por tanto tendremos que indicarle al compilador de C++ explÃcitamente que haga mangling al estilo de C. Para hacerlo habrá que modificar ligeramente la función :
*extern* "C" void incrementador(int &a)
{ a = a+1;
}
Volvemos a compilar el código C++ para obtener el archivo objeto. Ahora al compilar el programa en Pascal todo funciona correctamente. En realidad si hubiéramos compilado con el mangling de C++ tendrÃamos que haber llamado la función __incrementador_FRi.
Podemos indicar al compilador cual es el nombre real de la función que estamos importando. Para hacerlo sólo hay que añadir la directiva name después de external y el nombre, sensible a mayúsculas, de la función. Por ejemplo si compilamos el primer ejemplo de código en C++ tendremos que llamar la función por el nombre _incrementador__FRi. La declaración de la función quedarÃa de la forma siguiente :
procedure Incrementador(var a : Longint); cdecl; external name '_incrementador__FRi';
En este caso el nombre que demos a la función no se tiene en cuenta ya que se tiene en cuenta el valor que indica name. PodÃamos haberla declarado como INCREMENTADOR y todo hubiera funcionado igual. Este método lo podemos emplear siempre para ignorar el mangling por defecto de la convención de llamada.
Exportación de funciones en archivos objeto
Cuando compilamos un archivo, ya sea una unit o un programa, con FreePascal siempre se obtiene un archivo objeto. Este archivo contiene las declaraciones del programa, variables, etc una vez se han compilado. Para poder exportar funciones en este archivo objeto tendremos que emplear una unit, ya que los programas definen sÃmbolos que nos pueden conllevar problemas a la hora de enlazar con otros programas.
Las funciones que queramos exportar tendrán que llevar la directiva export para indicar al compilador que esta función va a ser exportada, en caso contrario no se podrÃa acceder a ella. Para evitar problemas con el mangling de las funciones es recomendable declarar las funciones que exportemos como cdecl o bien definir un nombre o alias para la función :
unit PruebaObj; interface implementation procedure incrementar(var a : integer); export; stdcall; [Alias : 'increm' ]; begin a := a + 1; end; end.
Cuando referenciemos a esta función desde código C, por ejemplo, podremos emplear el nombre increm, sensible a mayúsculas, que hemos declarado en la estructura del alias.
Hay que tener en cuenta de que las funciones y procedimientos exportados no deberÃan ser llamados por otras funciones puesto que emplean un mecanismo diferente para el paso de parámetros. Aunque es posible llamar funciones no exportadas dentro de funciones exportadas sin ningún problema. Para impedir que el programador llame a estas funciones se omite la declaración en interface y sólo se implementa dentro del bloque implementation. De esta forma no es posible llamar a la función, aun cuando esta sea incluida en un programa o unit.
Para llamar a una función exportada hay que emplear el mecanismo que hemos visto antes para importar funciones de archivos de objeto. Cada lenguaje de programación tiene sus mecanismos para importar archivos objeto, si es que tiene.
Hay que tener en cuenta que si una función exportada llama funciones que se encuentran en otras units también necesitaremos sus archivos objeto. Por ejemplo la función Writeln se encuentra definida en la unit System y necesitaremos el archivo System.o (o bien, System.ow). Esta unit no es necesario añadirla en la cláusula uses ya que se añade siempre automáticamente por el compilador.
Hay otras restricciones por lo que a las funciones exportadas concierne. Por ejemplo los ShortString (ni String[n]) no se exportan correctamente y es recomendable emplear PChar o bien, si es para emplearse en programas y units compilados con FreePascal, el tipo AnsiString que sà que se exporta correctamente. Finalmente hay que tener en cuenta de que los nombres de los tipos de datos, como los enteros que hemos visto antes, pueden variar de un lenguaje a otro.
Exportar e importar variables
De la misma forma que podemos importar y exportar funciones y procedimientos también podemos exportar e importar variables. Para exportar una variable emplearemos de nuevo una unit. A diferencia del caso anterior no hay problema en acceder a ella por lo que podremos incluirla en la sección interface de una unit sin ningún problema.
Es muy recomendable que el nombre de la variable se guarde en el archivo objeto con mangling de C. Para hacerlo añadiremos la directiva cvar después de la declaración de una única variable. No es posible emplear un mismo cvar para varias variables pero sà varios cvar para cada variable declarada. En este caso la declaración será sensible a las mayúsculas aunque en el código en Pascal podremos referenciarla como queramos. La unit que emplearemos de ejemplo es la siguiente :
unit ExportarVariable; interface implementation var valor : Longint; cvar; procedure Cuadrado(a : Longint); *cdecl*; *export*; // Exportamos la función begin Valor := a*a; end; end.
Para importar una variable tenemos dos formas. La primera se basa en aprovecharse del método de mangling de C. Para hacerlo habrá que añadir la directiva cvar seguida de la directiva external, que indica al compilador que no reserve memoria para esta variable pues es externa. En este caso el nombre de declaración es sensible a las mayúsculas aunque desde el código podremos referirnos a ella sin distinción de mayúsculas.
La otra forma de declarar una variable externa permite emplear el identificador que queramos pero implica conocer el mangled name de la variable. En C las variables llevan un guión bajo delante del identificador. De esta forma podemos declarar la variable externa de nuestro archivo objeto.
{$LINK EXPORTARVARIABLE.OW}
var
valor: Longint; *cvar*; *external*; // Valor, VALOR u otros no son válidos
alias : Longint; *external* name '_valor';
Como se observa, las variables valor y alias se referirán a la misma variable de forma que cualquier cambio en esta se apreciará en el valor de las dos variables y viceversa, las asignaciones a cualquiera de ellas dos afectarán al valor de la otra. Finalmente vamos a importar la función Cuadrado que hemos exportado en la unit.
procedure Cuadrado(a : Longint); *cdecl*; *external*; // quadrat, o QUADRAT no valen
El programa completo es el siguiente :
program ImportarFuncionesyVariables;
{$L EXPORTARVARIABLE.OW}
procedure Cuadrado(a : Longint); *cdecl*; *external*;
var
valor : Longint; *cvar*; *external*;
alias : Longint; *external* name '_valor';
begin
Cuadrado(6);
Writeln(valor, ' = ', alias);
end.
LibrerÃas de enlace dinámico
Hasta ahora hemos visto mecanismos de enlace estático con funciones y procedimientos y variables. La existencia de la función y su posterior enlace con el código se ha resuelto todo el rato antes de ejecutar el programa, en tiempo de compilación.
Las librerÃas de enlace dinámico (con extensión DLL en Windows y so en Linux) permiten realizar el enlace en tiempo de ejecución.
El enlace dinámico tiene varias ventajas. El archivo, al no ir enlazado estáticamente, puede ser sustituido por otro que corrija errores o mejoras en el algoritmo, siempre que se conserven los nombres y los parámetros de las funciones. De esta forma se puede actualizar la librerÃa. En el caso del enlace estático habrÃamos tenido que recompilar el programa para actualizarlo. De esta forma, el programador la mayorÃa de veces sólo tiene que preocuparse en cuánto y cómo llamar a la función más que no como ha sido implementada, pues los cambios en la implementación no implican recompilar el programa de nuevo.
Creación de una librerÃa de enlace dinámico
La creación de una librerÃa de enlace dinámico es parecida a un programa. Para empezar es necesario que la librerÃa empiece con la palabra reservada library que indica al compilador que lo que se encontrará responde a la forma de una librerÃa. Hay que acompañar a la palabra reservada de un identificador que no es necesario que tenga el mismo nombre que el archivo.
Las funciones y procedimientos se declararán de la forma habitual, sin export, y especificando la convención de llamada. Es muy habitual en Windows la convención stdcall para funciones en DLL por lo que se recomienda esta convención.
Para exportar las funciones emplearemos la cláusula exports (con ese final, es importante) seguida de los identificadores de función o procedimiento que queramos exportar y, opcionalmente pero más que recomendable, el nombre de exportación de la función. Este será el nombre con el que llamaremos después la función o procedimiento de la librerÃa. En caso contrario, si no lo especificamos se empleará el mangling por defecto de FreePascal, que en el caso concreto de las DLL es todo el nombre de la función toda en mayúsculas.
library CuadradosyCubos; function Cuadrado(a : longint) : longint; *stdcall*; begin Cuadrado := a*a; end; function Cubo(a : longint) : longint; *stdcall*; begin Cubo := a**3; end; exports Cuadrado name 'Cuadrado', Cubo name 'Cubo'; end.
En este ejemplo estamos exportando dos funciones Cuadrado y Cubo. Una vez compilada la librerÃa obtendremos un archivo .DLL que ya podrá ser llamado desde nuestro programa.
Las funciones que exporte la librerÃa no tienen porqué estar declaradas forzosamente dentro de la librerÃa. Pueden estar declaradas en la interfaz de una unit que se encuentre dentro de la cláusula uses después de la cabecera library.
Importación de funciones en librerÃas de enlace dinámico
La forma de importar las funciones y los procedimientos es parecida. Ahora no es necesario especificar ninguna directiva como $LINK y el compilador tampoco detectará si la función es incorrecta. Por este motivo es importante especificar los parámetros correctamente conjuntamente con la convención de llamada adecuada.
En este caso la directiva external tiene que ir seguida del nombre de la librerÃa y puede ir seguida de una directiva name que especifique el nombre. En caso contrario se empleará el mangling habitual.
program FuncionesDLL;
const
NOMBREDLL = 'CuadradosyCubos';
function Cubo(a : longint) : longint; *stdcall*; *external* NOMBREDLL name 'Cubo';
function Cuadrado(a : longint) : longint; *stdcall*; *external* NOMBREDLL name 'Cuadrado';
var
a : integer;
begin
Write('Introduzca un número : '); Readln(a);
Writeln('Cuadrado : ', Cuadrado(a), ' Cubo : ', Cubo(a));
end.
Como se puede ver, es posible y muy recomendable, emplear constantes cuando nos referimos a una misma librerÃa a fin de evitar errores tipográficos. En este ejemplo hemos supuesto que la librerÃa era CUADRADOSYCUBOS.DLL. También podemos observar que no importa el orden en el que se importan las funciones. En este caso no hemos especificado la extensión en NOMBREDLL, que por defecto en Win32 es .DLL, pero es posible especificarla sin ningún problema.
Este programa sólo funcionará si es capaz de encontrar la librerÃa CUADRADOSYCUBOS.DLL en alguno de estos directorios :
- El directorio donde se encuentra el archivo ejecutable.
- El directorio actual del sistema de archivos.
- El directorio de Windows. Habitualmente C:\WINDOWS
- El subdirectorio SYSTEM de Windows. Habitualmente C:\WINDOWS\SYSTEM
- Los directorios de la variable PATH.
En caso de no existir o que no se encontrará la librerÃa en alguno de estos directorios, Windows nos mostrarÃa un error y no podrÃamos ejecutar el programa. Igualmente pasarÃa si la función que importemos de una DLL no existiera. En este aspecto es importante remarcar el uso de la directiva name para importar correctamente las funciones.
Importación y exportación de variables en DLL
Es posible en FreePascal, y Delphi, exportar variables en librerÃas DLL aunque sólo el primero permite importarlas mediante la sintaxis. El método para hacerlo es muy similar a exportar e importar funciones. Por ejemplo, la librerÃa siguiente exporta una función que inicializará la variable exportada.
library VarExport; var variable_exportada : integer; procedure Inicializar; begin variable_exportada := 10; end; exports Inicializar name 'Inicializar', variable_exportada name 'variable_exportada'; end.
El programa siguiente importa la función y la variable. Supongamos que la DLL recibe el nombre VarExport.DLL.
program ImportarVar;
const
VarExportDLL = 'VarExport';
var
variable : integer; external VarExportDLL name 'variable_exportada';
procedure Inicializar; external VarExportDLL name 'Inicializar';
begin
Inicializar;
Writeln(variable); {10}
end.
Importación dinámica de funciones en librerÃas DLL (sólo Win32)
Mediante la API de Win32, interfaz de programación de aplicaciones, podemos importar funciones de librerÃas de forma programática. De esta forma podemos controlar si la función existe o si la librerÃa se encuentra en el sistema y dar la respuesta adecuada ante estas situaciones.
Tendremos que emplear tres funciones y varias variables. Por suerte FreePascal incorpora la unit Windows donde están declaradas la mayor parte de tipos de datos e importadas la mayor parte de funciones de la Win32 API.
La función LoadLibrary, que recibe como parámetro el nombre de la librerÃa en una cadena terminada en nulo, devolverá cero si esta librerÃa no existe. En caso contrario devuelve un valor distinto de cero.
La función GetProcAddress nos devolverá un puntero nil si la función que importamos no existe. En caso contrario nos dará un puntero que podremos enlazar en una variable de tipo función a fin de poderla ejecutar. Finalmente la función FreeLibrary libera la memoria y descarga la librerÃa si es necesario.
Vamos a ver un ejemplo con la función Cuadrado de la primera librerÃa de ejemplo.
program FuncionDLLDinamica;
uses Windows;
const
NOMBREDLL = 'CuadradosYCubos.dll';
NOMBREFUNCION = 'Cuadrado';
type
TCuadrado = function (a : longint) : longint; *stdcall*;
var
a : integer;
Cuadrado : TCuadrado;
Instancia : HMODULE; // Tipo de la unit Windows
begin
Write('Introduzca un numero : '); Readln(a);
// Intentaremos importar la función
Instancia := LoadLibrary(PCHar(NOMBREDLL));
if Instancia <> 0 then
begin
// Hay que realizar un typecasting correctamente del puntero
Cuadrado := TCuadrado(GetProcAddress(Instancia, NOMBREFUNCION));
if @Cuadrado <> nil then
begin
Writeln('Cuadrado : ', Cuadrado(a));
end
else
begin
Writeln('ERROR - No se ha encontrado la función en ', NOMBREDLL);
end;
// Liberamos la librerÃa
FreeLibrary(Instancia);
end
else
begin
Writeln('ERROR - La librerÃa ', NOMBREDLL,' no se ha encontrado');
end;
end.
Puede parecer un código complejo pero no hay nada que no se haya visto antes. Comentar sólo que hay que hacer un amoldado del puntero que se obtiene con GetProcAddress antes de asignarlo correctamente al tipo función. Como nota curiosa se puede cambiar 'Cuadrado' de la constante NOMBREFUNCION por el valor 'Cubo' y todo funcionarÃa igual ya que Cubo y Quadrado tienen la misma definición en parámetros y convención de llamada. Sólo que en vez de obtener el cuadrado obtendrÃamos el cubo del número.
Llamada a funciones del API de Win32
Aunque la mayorÃa de funciones de la API se encuentran importadas en la unit Windows de vez en cuando tendremos que llamar alguna que no esté importada. El método es idéntico para cualquier DLL y sólo hay que tener en cuenta de que la convención de llamada siempre es stdcall.
Programación orientada a objetos
Programación procedimental vs programación orientada a objetos
Hasta ahora hemos visto un tipo de programación que podrÃamos llamar procedimental y que consiste en reducir los problemas a trozos más pequeños, funciones y procedimientos, y si es necesario agrupar estos trozos más pequeños con elementos en común en módulos, units y librerÃas.
Este modelo de programación, que parece muy intuitivo y necesario para programadores que no han visto la programación orientada a objetos (POO de ahora en adelante) tiene varios inconvenientes.
Para empezar, se basa en un modelo demasiado distinto a la forma humana de resolver los problemas. El ser humano percibe las cosas como elementos que suelen pertenecer a uno o más conjuntos de otros elementos y aplica los conocimientos que tiene de estos conjuntos sobre cada elemento. AsÃ, es evidente de que un ratón y un elefante son seres vivos y como seres vivos ambos nacen, crecen, se reproducen y mueren. En otro ejemplo, un alambre no es un ser vivo pero sabemos que es metálico y como elemento metálico sabemos que conduce bien la electricidad y el calor, etc. Esta asociación de ideas a conjuntos no es fácil de implementar en la programación procedimental ya que si bien un ser vivo es algo más amplio que el concepto de elefante no es posible implementar (siempre hablando de implementación de forma eficiente, claro) un sistema que dado un ser vivo cualquiera pueda simular el nacimiento, crecimiento, etc. básicamente porque cada ser vivo lo hace a su manera.
Llegados a aquÃ, se empieza a entrever más o menos el concepto de objeto. Es algo que pertenecerá a un conjunto y que al pertenecer en él poseerá sus cualidades a la vez que puede tener sus propias cualidades o adaptar las que ya tiene.
En qué se parecen una moto y un camión ? Bien, al menos ambos son vehÃculos y tienen ruedas. En qué se distinguen ? La moto sólo tiene dos ruedas y sólo puede llevar un tripulante (en el peor de los casos) mientras que el camión tiene cuatro ruedas y además dispone de un compartimiento para llevar materiales. Qué pasa si cogemos una moto y le añadimos una tercera rueda y un pequeño lugar para un segundo tripulante ? Pues que la moto se ha convertido en un sidecar. En qué se distinguen la moto y el sidecar ? Pues básicamente en sólo este añadido. En qué se parecen ? En todo lo demás. Por tanto no es exagerado decir que un sidecar hereda las propiedades de una moto y además añade nuevas cualidades, como el nuevo compartimiento.
Ya hemos visto la mayor parte de las propiedades de un objeto de forma bastante intuitiva que definen los objetos y que veremos más adelanta. Estas tres propiedades tienen nombre y son : la encapsulación, la herencia y el polimorfismo.
Encapsulación, herencia y polimorfismo
Encapsulación
Quizás ahora no queda muy claro qué quiere decir encapsulación. Básicamente consisten en que todas las caracterÃsticas y cualidades de un objeto están siempre definidas dentro de un objeto pero nunca fuera de los objetos. Es el mismo objeto quien se encarga de definir sus propiedades y en su turno las implementa. Siempre dentro del contexto de objeto que veremos.
Herencia
Hablamos de herencia cuando un objeto adquiere todas las caracterÃsticas de otro. Esta caracterÃstica permite construir jerarquÃas de objetos donde un segundo objeto hereda propiedades de otro y un tercero de este segundo de forma que el tercero también tiene propiedades del primero.
En el ejemplo anterior una moto es [heredera de] un vehÃculo y un sidecar es [heredera de] una moto. Las propiedades que definen a un vehÃculo también las encontraremos en un sidecar. En cambio a la inversa no siempre es cierto.
Polimorfismo
Esta palabra sólo significa que dado un objeto antepasado, o ancestro, si una acción es posible llevarla a cabo en este objeto (que se encontrará en la parte superior de la jerarquÃa de objetos) también se podrá llevar a cabo con sus objetos hijos pues la heredan. Pero cada objeto jerárquicamente inferior puede (que no tiene por qué) implementar esta acción a su manera.
Volviendo al ejemplo de los seres vivos : todos se reproducen. Algunos de forma asexual, dividiéndose o por gemación. De otros de forma sexual, con fecundación interna o externa, etc.
Resumiendo
Ahora que hemos visto más o menos cuales son las propiedades de los objetos podemos llegar a la conclusión que una de las ventajas directas de la POO es la reutilización del código y su reusabilidad es mayor.
Supongamos que tenemos un objeto que, por el motivo que sea, está anticuado o le falta algún tipo de opción. Entonces podemos hacer uno nuevo que herede de éste y que modifique lo que sea necesario, dejando intacto lo que no se tenga que retocar. El esfuerzo que habremos tenido que hacer es mucho menor al que hubiéramos tenido que hacer para reescribir todo el código.
Clases y objetos
De las distintas formas de aproximación a la POO que los lenguajes de programación han ido implementado a lo largo del tiempo, en Pascal encontraremos dos formas parecidas pero distintas en concepto : los objetos y las clases.
En este manual emplearemos las clases sin ver los objetos pues es una sintaxis y concepción mucho más parecida a la forma de entender la POO de C++ que se basa en clases.
Un objeto es la unión de un conjunto de métodos, funciones y procedimientos, y campos, las variables, que son capaces de heredar y extender los métodos de forma polimórfica. Para emplear un objeto necesitamos una variable de este objeto. Sólo si tiene métodos virtuales, que ya veremos que son, es necesario inicializar y destruir el objeto.
Las clases son como objetos que, a diferencia, no se pueden emplear directamente en una variable si no que la variable tiene como finalidad recoger una instancia, una copia usable para entendernos, de esta clase. Esta copia la devuelve el constructor y recibe el nombre de objeto ya que es el elemento real mientras que la clase es un elemento formal del lenguaje. Posteriormente habrá que liberar el objeto. Es necesario siempre obtener una instancia de la clase u objeto, en caso contrario no es posible trabajar con él. De ahora en adelante cuando hablemos de objetos estaremos hablando de instancias de clases y no del otro modelo de aproximación a la POO.
Las primeras clases
Antes de poder trabajar con la POO mediante clases tenemos que avisar al compilador de que emplearemos clases. Para hacerlo hay que emplear la directiva de compilador {$MODE OBJFPC}^5 <#sdfootnote5sym>.
Implementaremos una clase muy simple que permita realizar operaciones simples a partir de dos variables de la clase. La clase se llamara TOperaciones. Las clases hay que definirlas como si fueran tipos de datos ya que, habitualmente, con lo que se trabaja es una instancia de una clase, un objeto, no con la clase en sÃ.
Para declarar una clase hay que emplear la palabra reservada class y una estructura algo parecida a un record. Hay que especificar también de quien es heredera esta clase. En Pascal todas las clases tienen que heredar de alguna otra y la clase superior a todas se llama TObject. Esta es la declaración.
{$MODE OBJFPC}
type
TOperaciones = class ( TObject )
Dentro de esta declaración primero declararemos los campos, las variables de la clase. En nuestro caso declararemos los operandos de las operaciones binarias (operaciones de dos operandos) y les daremos los nombres a y b. La declaración se hace de la misma forma que cuando declaramos variables normalmente. Las declararemos de tipo entero. También declararemos una variable, Resultado, en la cual guardaremos el valor de las operaciones.
Ahora hay que declarar algún método (procedimientos y funciones de la clase). En nuestro ejemplo implementaremos la operación suma en un procedimiento llamado Suma que sumará a y b y almacenará el resultado en la variable Resultado. La interfaz de la clase quedará asÃ.
type TOperaciones = class ( TObject ) a, b, Resultado : integer; procedure Suma; end;
Obsérvese que Suma no necesita parámetros ya que trabajará con a y b. Una vez tenemos la interfaz habrá que implementar los métodos. Para hacerlo lo haremos como normalmente pero cuidando de que el nombre del método vaya precedido del nombre de la clase. La implementación de Suma serÃa la siguiente :
procedure TOperaciones.Suma; begin Resultado := a + b; end;
Como se ve, podemos trabajar con las variables de la clase dentro de la misma clase sin necesidad de redeclararlas. Esto es posible pues la clases tienen una referencia interna que recibe el nombre de Self. Self se refiere a la misma clase por lo que la siguiente definición es equivalente a la anterior :
procedure TOperaciones.Suma; begin Self.Resultado := Self.a + Self.b; end;
El identificador Self lo emplearemos cuando haya alguna ambigüedad respecto a las variables o métodos que estamos empleando. En general no suele ser necesario.
Para poder emplear una clase es necesario que la instanciemos en una variable del tipo de la clase. Para hacerlo necesitamos emplear el constructor de la clase, por defecto Create. Finalmente cuando ya no la necesitemos más hay que liberarla con el método Free. Estos métodos no los implementamos, de momento, pues ya vienen definidos en TObject. El código del bloque principal para emplear la clase es el siguiente :
var Operaciones : TOperaciones; begin Operaciones := TOperaciones.Create; // Instancia de la clase Operaciones.a := 5; Operaciones.b := 12; Operaciones.Suma; Writeln(Operaciones.Resultado); Operaciones.Free; // Liberamos la instancia end.
Por defecto el constructor Create no toma parámetros. Su resultado es una instancia de una clase que almacenamos en Operaciones, como si se tratara de una especie de puntero. Podemos acceder a los campos y métodos de la clase como si de un record se tratara. No hay que olvidar de liberar la instancia pues dejarÃamos memoria reservada sin liberar lo cual podrÃa afectar al rendimiento posterior del sistema. Téngase en cuenta que una vez liberada la instancia ya no es posible acceder a sus campos ni a sus métodos. Hacerlo resultarÃa en un error de ejecución de protección general de memoria.
Hacer clases más robustas
La clase que hemos diseñado anteriormente tiene unos cuantos problemas. Para empezar, el programador puede acceder a la variable Resultado y modificarla a su gusto. Esto podrÃa falsear el resultado de la operación. Por otro lado, en una hipotética operación División el programador podrÃa caer en la tentación de poner un divisor con valor cero lo que provocarÃa un error de ejecución al realizarse la operación.
Hay varias formas de implementar clases robustas que sean capaces de reaccionar delante de los errores del programador y del usuario. Las clases tienen varios mecanismos para proteger al programador de sus propios errores : los ámbitos de clase y las propiedades.
Ã?mbitos de la clase o grados de visibilidad
Las clases definen varios grados de visibilidad. Estos grados de visibilidad regulan en cierta forma el acceso a los elementos del objeto, campos y métodos, desde otros ámbitos del código. Los grados de visibilidad fundamentales son private y public.
Los métodos y campos con visibilidad private sólo son accesibles dentro de la misma clase, o sea, en los métodos que esta clase implementa. Los elementos public son accesibles desde cualquier lugar desde el cual se tenga acceso a la instancia de la clase.
Después hay un grado intermedio que recibe el nombre de protected. A diferencia de private, protected, permite el acceso a los miembros de la clase pero sólo en las clases que hereden de la clase, aspecto que en el grado private no es posible. Como veremos más adelante, las clases inferiores pueden modificar los grados de visibilidad de elementos a los cuales tienen acceso en herencia, protected y public.
En nuestra clase protegeremos la variable Resultado y la estableceremos private. La resta de clase será public. Los atributos se fijan directamente en la declaración de la clase :
type TOperaciones = class ( TObject ) public a, b : integer; // Esto es publico procedure Suma; // Esto también private Resultado : integer; // Esto en cambio es privado end;
Ahora, tenemos un inconveniente, no es posible acceder a la variable Resultado fuera de la clase. Una solución puede pasar por definir una función que devuelva el valor de Resultado. Una solución más elegante es emplear propiedades.
Propiedades de una clase
Las propiedades son una aproximación a la POO muy eficaz que fueron introducidas en Delphi. Son unos miembros especiales de las clases que tienen un comportamiento parecido a un campo, o sea una variable, pero además es posible establecer la posibilidad de lectura y/o escritura. Lo más interesante de las propiedades es la posibilidad de asociar un método con las operaciones de lectura y escritura con lo cual podemos escribir clases eficientes y robustas.
Las propiedades se definen de la forma siguiente :
property NombrePropiedad : tipoDato read Variable/Metodo write Variable/Metodo;
Téngase en cuenta de que las propiedades sólo se pueden declarar dentro de una clase. Si omitimos la parte write tendremos una propiedad de sólo lectura mientras que si omitÃs la parte de read tendremos una propiedad de sólo escritura. Este caso es técnicamente posible pero es poco útil, en general.
Para el ejemplo anterior renombraremos la variable Resultado a FResultado con lo cual tendremos que modificar el método Suma donde habrá que cambiar Resultado por FResultado.
type TOperaciones = class ( TObject ) private FResultado : integer; public a, b : integer; procedure Suma; end; procedure TOperaciones.Suma; begin FResultado := a + b; end;
Aunque el orden es indiferente, es habitual poner los atributos de visibilidad en el orden : private, protected y public. Esto es asà porque en miembros públicos se emplean propiedades privadas pero nunca al revés. Declaramos la propiedad pública Resultado de sólo lectura sobre la variable FResultado.
property Resultado : Integer read FResultado;
Cuando leamos el valor de Resultado lo que estamos leyendo es el valor FResultado. Si intentamos modificar su valor el compilador nos advertirá de que no es posible pues no hemos especificado esta posibilidad. La definición de la clase quedarÃa tal como sigue :
type TOperaciones = class ( TObject ) private FResultado : integer; public a, b : integer; property Resultado : integer read FResultado; procedure Suma; end;
Tal como hemos visto antes en la definición de la declaración de propiedades es posible indicar que la lectura y escritura se realice mediante un método. Si el método es de lectura emplearemos una función del tipo de la propiedad. Si el método es de escritura emplearemos un procedimiento con un único parámetro del tipo de la propiedad.
El programa siguiente es un ejemplo inútil del uso de las propiedades :
program EjemploPropiedades;
{$MODE OBJFPC}
type
TEjemplo = class (TObject)
private
function LeerPropiedad : Integer;
procedure EscribirPropiedad(Valor : Integer);
public
property Propiedad : Integer read LeerPropiedad write EscribirPropiedad;
end;
function TEjemplo.LeerPropiedad : Integer;
var
i : integer;
begin
Randomize; // Para que los números sean aleatorios
i := Random(10);
Writeln('Leyendo la propiedad. Devolviendo un ', i);
LeerPropiedad := i;
end;
procedure TEjemplo.EscribirPropiedad(Valor : Integer);
begin
Writeln('Escribiendo la propiedad. Ha asignado el valor ', Valor);
end;
var
Ejemplo : TEjemplo;
begin
Ejemplo := TEjemplo.Create;
Writeln('Devuelto : ', Ejemplo.Propiedad); // Leemos -> LeerPropiedad
Ejemplo.Propiedad := 15; // Escribimos -> EscribirPropiedad
Ejemplo.Free;
end.
Una posible salida del programa es la siguiente :
Leyendo la propiedad. Devolviendo un 6 Devuelto : 6 Escribiendo la propiedad. Ha asignado el valor 15
Obsérvese que al leer la propiedad hemos llamado al método LeerPropiedad que ha escrito la primera lÃnea. Una vez se ha devuelto un valor aleatorio Writeln escribe el valor de la propiedad. Después cuando asignamos la propiedad se ejecuta el método EscribirPropiedad.
Hay que observar que las referencias a métodos en las propiedades se hacen igual que las referencias en variables. También es posible combinar lectura de métodos con escritura de variables y viceversa sin ningún tipo de problema. De hecho la combinación mas usual suele ser la de una propiedad que lee una variable privada y que activa un método privado cuando se modifica.
Finalmente sólo hay que comentar que la declaración de métodos de escritura permite que se pase el parámetro como un parámetro constante. De forma que la declaración anterior de EscribirPropiedad podrÃa ser asà :
procedure EscribirPropiedad(const Valor : Integer);
La posterior implementación habrÃa necesitado también la palabra reservada const.
La función Randomize inicializa los números aleatorios y la función Random(n) devuelve un entero aleatorio entre 0 y n-1.
Propiedades indexadas
Las propiedades indexadas son un tipo de propiedad que permite acceder y asignar datos a través de un Ãndice como si de un array se tratara. A diferencia de los arrays usuales, el Ãndice no tiene porque ser un entero, también es válido un real, cadenas, caracteres, etc.
Definir una propiedad indexada no es más complicado que definir una propiedad normal. Simplemente los métodos write y read añaden los parámetros del Ãndice. Veamos el ejemplo siguiente:
type TUnaClasse = class (TObject) private procedure SetIndex(a, b : integer; c : Extended); function GetIndex(a, b : integer) : Extended; public property Index[a, b : integer] : Extended read GetIndex write SetIndex; end;
En este caso si accedemos a la propiedad Index[n, m] obtendremos un real de tipo Extended. Igualmente, las asignaciones a Index[n, m] tienen que ser de tipo Extended. También podÃamos haber declarado un tipo string como Ãndice de la propiedad.
type
TUnaClasse = class (TObject)
private
procedure SetIndex(a : String; b : Integer);
function GetIndex(a : String) : Integer;
public
property Index[a : String] : integer read SetIndex write SetIndex;
end;
Ahora serÃa legal la sentencia siguiente :
UnaClasse.Index['hola'] := 10;
13.5.4.Propiedades por defecto
Si al final de una propiedad indexada añadimos la directiva default entonces la propiedad se vuelve por defecto. Obsérvese la declaración siguiente :
property Index[a : String] : integer read SetIndex write SetIndex; default;
Ahora serÃa legal la asignación siguiente :
UnaClasse['hola'] := 10;
Como se deduce sólo puede haber una propiedad indexada por defecto en cada clase y las clases descendientes no pueden modificarla.
Herencia y polimorfismo
Ventajas de la herencia en la POO
Los objetos, como hemos comentado antes, son capaces de heredar propiedades de otros objetos. Algunos lenguajes como el C++ permiten que una misma clase herede propiedades de uno o más objetos. La herencia múltiple no deja de tener sus problemas, como por ejemplo la duplicidad de identificadores entre clases. Es por este motivo que muchos lenguajes simplifican la herencia a una sola clase. O sea, las clases pueden heredar como máximo de otra clase. Esto provoca que las jerarquÃas de objetos sean totalmente jerárquicas sin estructuras interrelacionadas entre ellas. La aproximación a la POO que se hace en FreePascal se basa en la herencia simple por lo que una clase siempre es heredera de una sola cada vez. Esto no impide que las clases tengan numerosos descendientes.
La clase TObject
Tal como hemos comentado antes, FreePascal implementa la herencia simple de forma que en algún momento llegaremos a la clase más superior. Esta clase que ya hemos visto se llama TObject. En FreePascal todas las clases descienden directa o indirectamente (ya sea por transitividad de herencia) de TObject.
La clase TObject define bastantes métodos pero los más interesantes son sin duda el constructor Create y el destructor Destroy. El destructor Destroy no se llama nunca directamente. En vez de ello se emplea otro método de TObject que es Free. Free libera la clase incluso si no ha sido asignada por lo que es algo más seguro que Destroy.
Diseñar clases heredadas
Una de las ventajas de diseñar clases heredadas es la de poder dar soluciones concretas derivadas de soluciones más genéricas o comunas. De esta forma nuestras clases se pueden especializar.
La tercera propiedad de los objetos, el polimorfismo, permite que bajo el nombre de un mismo método se ejecute el código adecuado en cada situación. Los métodos que incorporan esta posibilidad reciben el nombre de métodos dinámicos, en contraposición a los estáticos, o más habitualmente métodos virtuales.
Antes de emplear métodos virtuales habrá que ver ejemplos sencillos, a la par que poco útiles pero ilustrativos, del funcionamiento de la herencia.
Herencia de clases
Para poder heredar las caracterÃsticas de una clase, métodos y propiedades, habrá que tener alguna clase de donde heredar. Con esta finalidad declararemos la clase TPoligono que se encargará teóricamente de pintar un polÃgono. De hecho no pintará nada, sólo escribirá un mensaje en la pantalla. Se supone que el programador no empleará nunca esta clase sino que sus derivados por lo que su único método PintarPoligono será protected. Hay que pasar un parámetro n indicando el número de lados del polÃgono. La clase es la siguiente :
type
TPoligono = class (TObject)
protected
procedure PintarPoligono(n : integer);
end;
procedure TPoligono.PintarPoligono(n : integer);
begin
Writeln('Pintar polÃgono de ', n,' lados');
end;
Hemos supuesto que el programador no trabajará nunca con objetos de tipo TPoligono sino que sólo sus derivados. Por eso definimos dos clases nuevas derivadas de TPoligono : TTriangulo y TCuadrado.
type TTriangulo = class (TPoligon) public procedure PintarTriangulo; end; TCuadrado = class (TPoligon) public procedure PintarCuadrado; end; procedure TTriangle.PintarTriangulo; begin PintarPoligono(3); end; procedure TQuadrat.PintarCuadrado; begin PintarPoligono(4); end;
Tal como se ve, los métodos PintarTriangulo y PintarCuadrado no son nada más que casos más concretos de PintarPoligono. Supongamos ahora que definimos una clase llamada TPoligonoLleno que además de pintar el polÃgono además lo rellena de color. Será posible aprovechar el código de PintarPoligono de TPoligono. Lo que haremos es ampliar el método PintarPoligono en la clase TPoligonoLleno que derivará de TPoligono. La declaración e implementación es la que sigue :
type
TPoligonoLleno = class (TPoligono)
protected
procedure PintarPoligono(n : integer);
end;
procedure TPoligonoLleno.PintarPoligono(n : integer);
begin
inherited PintarPoligono(n);
Writeln('Llenando el polÃgono de color');
end;
Para llamar al método de la clase superior que hemos redefinido en la clase inferior hay que emplear la palabra reservada inherited. La sentencia inherited PintarPoligono(n) llama a TPoligono.PintarPoligono(n). De otra forma se entenderÃa como una llamada recursiva y no es el caso.
Finalmente declararemos dos clases nuevas TTrianguloLleno y TCuadradoLleno que descienda de TPoligonoLleno.
program EjemploHerencia;
{$MODE OBJFPC}
type
TPoligono = class (TObject)
protected
procedure PintarPoligono(n : integer);
end;
TTriangulo = class (TPoligono)
public
procedure PintarTriangulo;
end;
TCuadrado = class (TPoligono)
public
procedure PintarCuadrado;
end;
TPoligonoLleno = class (TPoligono)
protected
procedure PintarPoligono(n : integer);
end;
TTrianguloLleno = class (TPoligonoLleno)
public
procedure PintarTriangulo;
end;
TCuadradoLleno = class (TPoligonLleno)
public
procedure PintarCuadrado;
end;
// métodos de TPoligono
procedure TPoligono.PintarPoligono(n : integer);
begin
Writeln('Pintar polÃgono de ', n,' lados');
end;
// Métodos de TTriangulo
procedure TTriangulo.PintarTriangulo;
begin
PintarPoligono(3);
end;
// Método de TCuadrado
procedure TCuadrado.PintarCuadrado;
begin
PintarPoligono(4);
end;
// Métodos de TPoligonoLleno
procedure TPoligonoLleno.PintarPoligono(n : integer);
begin
inherited PintarPoligono(n);
Writeln('Llenando el polÃgono de color');
end;
// Métodos de TTrianguloLleno
procedure TTrianguloLleno.PintarTriangulo;
begin
PintarPoligono(3);
end;
// Métodos de TQuadratPle
procedure TCuadradoLleno.PintarCuadrado;
begin
PintarPoligono(4);
end;
var
Triangulo : TTriangulo;
TrianguloLleno : TTrianguloLleno;
Cuadrado : TCuadrado;
CuadradoLleno : TCuadradoLlen;
begin
Triangulo := TTriangulo.Create;
Triangulo.PintarTriangulo;
Triangulo.Free;
Writeln; // Una linea de separación
TrianguloLleno := TTrianguloLleno.Create;
TrianguloLleno.PintarTriangulo;
TrianguloLleno.Free;
Writeln;
Cuadrado := TCuadrado.Create;
Cuadrado.PintarCuadrado;
Cuadrado.Free;
Writeln;
CuadradoLleno := TCuadradLleno.Create;
CuadradoLleno.PintarCuadrado;
CuadradoLleno.Free;
end.
Como se ve, la POO exige algo más de código pero no implica que los archivos compilados resultantes sean mayores. Simplemente la sintaxis es algo más compleja y se requieren algo más de código.
El resultado del programa anterior serÃa :
Pintar polÃgono de 3 lados Pintar polÃgono de 3 lados Llenando el polÃgono de color Pintar polÃgono de 4 lados Pintar polÃgono de 4 lados Llenando el polÃgono de color
Problemas de los métodos estáticos
Vamos a ver el problema de los métodos estáticos en POO. Supongamos que definimos una clase TVehiculo con un método público Ruedas. Este método será una función que devolverá el número de ruedas del vehÃculo. Para el caso de TVehiculo al ser genérico devolveremos -1. Declararemos también dos clases descendientes de TVehiculo llamadas TMoto y TCoche que también implementan la función Ruedas. En este caso el resultado será 2 y 4 respectivamente.
type TVehiculo = class (TObject) public function Ruedas : integer; end; TMoto = class (TVehiculo) public function Ruedas : integer; end; TCoche = class (TVehiculo) public function Ruedas : integer; end; function TVehiculo.Ruedas : integer; begin Ruedas := -1; end; function TMoto.Ruedas : integer; begin Ruedas := 2; end; function TCoche.Ruedas : integer; begin Ruedas := 4; end;
Si declaramos una variable del tipo TVehiculo y la instanciamos con los constructores de TMoto o TCoche entonces podremos acceder al método Ruedas ya que TVehiculo lo lleva implementado. Esto es sintácticamente correcto ya que las clases TMoto y TCoche descienden de TVehiculo y por tanto todo lo que esté en TVehiculo también está en TMoto y TCoche. La forma inversa, pero, no es posible. Por este motivo es legal realizar instancias de este tipo.
Supongamos el programa siguiente :
var UnVehiculo : TVehiculo; UnCoche : TCoche; begin UnVehiculo := TVehiculo.Create; Writeln(UnVehiculo.Ruedas); UnVehiculo.Free; UnCoche := TCoche.Create; Writeln(UnCoche.Ruedas); UnCoche.Free; UnVehiculo := TCoche.Create; Writeln(UnVehiculo.Ruedas); UnVehiculo.Free; end.
El resultado del programa tendrÃa que ser el siguiente :
-1 44
Pero al ejecutarlo obtenemos :
-1 4-1
Qué es lo que ha fallado ? Pues ha fallado el hecho de que el método Ruedas no es virtual, es estático. Este comportamiento es debido a la forma de enlazar los métodos. Los métodos estáticos, o no virtuales, se enlazan en tiempo de compilación. De esta forma las variables de tipo TVehiculo siempre ejecutarán TVehiculo.Ruedas aunque las instanciemos con los constructores de clases descendientes . Esto es asà para evitar que una clase inferior cambiara de visibilidad el método. A qué método tendrÃa que llamar el compilador ? Por ejemplo, si TCoche.Ruedas fuera privado a quien habrÃa tenido que llamar TVehiculo ?
Para superar este problema los lenguajes orientados a POO nos aportan un nuevo tipo de métodos llamados métodos virtuales.
Los métodos virtuales
Qué es lo que queremos resolver con los métodos virtuales exactamente ? Queremos que dada una instancia de la clase superior a partir de clases inferiores podamos ejecutar el método de la clase superior pero con la implementación de la clase inferior. O sea, dada una variable del tipo TVehiculo creamos la instancia con TMoto, por ejemplo, queremos que la llamada a Ruedas llame a TMoto ya que es la clase con la que la hemos instanciado. Porque es posible ? Básicamente porque los métodos virtuales se heredan como los otros métodos pero su enlace no se resuelve en tiempo de compilación sino que se resuelve en tiempo de ejecución. El programa no sabe a qué método hay que llamar, sólo lo sabe cuando se ejecuta. Esta caracterÃstica de los objetos y las clases recibe el nombre de vinculación retardada o tardÃa.
Para indicar al compilador de que un método es virtual lo terminaremos con la palabra reservada virtual. Sólo hay que hacerlo en la declaración de la clase. Modificamos TVehiculo para que Ruedas sea virtual.
type
TVehiculo = class (TObject)
public
function Ruedas : integer; virtual;
end;
Si en una clase definimos un método virtual y queremos que en sus clases descendientes también lo sean tendremos que emplear la palabra reservada override. Los métodos override indican al compilador que son métodos virtuales que heredan de otros métodos virtual.
TMoto = class (TVehiculo) public function Ruedas : integer; override; end; TCoche = class (TVehiculo) public function Ruedas : integer; override; end;
Esto indica al compilador que las funciones Ruedas de TCoche y TVehiculo son virtuales. El resultado del programa ahora será el que esperábamos :
-1 44
Téngase en cuenta de que si en una clase descendiente no hace el método override entonces este método volverá otra vez a ser estático, para esta clase y sus descendientes. Igualmente, si lo establecemos de nuevo virtual será virtual para las clases que desciendan. Por ejemplo, suprimiendo la directiva override de la declaración de TCoche el compilador nos advertirá que estamos ocultando una familia de métodos virtuales. El resultado será como si hubiéramos empleado métodos estáticos. Igual pasa si lo declaramos virtual de nuevo. En cambio, la clase TMoto, que tiene Ruedas virtual, funcionarÃa correctamente. El resultado del programa siguiente :
var UnVehiculo : TVehiculo; begin UnVehiculo := TCoche.Create; Writeln(UnVehiculo.Ruedas); UnVehiculo.Free; UnVehiculo := TMoto.Create; Writeln(UnVehiculo.Ruedas); UnVehiculo.Free; end.
serÃa :
-1 2
ya que ahora TCoche le hemos suprimido override y no es nada más que un método estático y el compilador lo enlaza directamente con TVehiculo. Es posible dentro de los métodos override llamar al método superior mediante inherited.
Los métodos dinámicos
Si en vez de emplear virtual empleamos dynamic entonces habremos declarado un método dinámico. Los métodos virtual están optimizados en velocidad mientras que los dinámicos están optimizados en tamaño del código (producen un código compilado menor) según las especificaciones de Delphi.
En FreePascal no están implementados y emplear dynamic es como escribir virtual, pues se admite por compatibilidad con Delphi.
Clases descendientes de clases con métodos override
El funcionamiento es parecido a las clases que derivan de métodos virtual. Si no añadimos override a la declaración el método será estático y se enlazará con el primer método virtual u override que encuentre. Supongamos que definimos TCochecito, por ejemplo, que deriva de TCoche.TCochecito.Ruedas queremos que devuelva 3.
type TCochecito = class (TCoche) public function Ruedas : integer; override; end; function TCochecito.Ruedas : integer; begin Ruedas := 3; end;
Podemos trabajar con esta clase de la forma siguiente :
var UnVehiculo : TVehiculo; begin UnVehiculo := TCochecito.Create; Writeln(UnVehiculo.Ruedas); UnVehiculo.Free; end.
Esto es posible ya que si TCoche desciende de TVehiculo y TCochecito desciende de TCoche entonces TCochecito también desciende de TVehiculo. Es la transitividad de la herencia.
Al ejecutar este código el resultado es 3. Pero si suprimimos override de TCochecito.Ruedas entonces el resultado es 4 pues se enlaza estáticamente con el primer método virtual que encuentra hacia arriba de la jerarquÃa de objetos, en este caso de TCoche.Ruedas.
Las clases abstractas
Una clase abstracta es una clase con uno o más métodos abstractos. Qué son los métodos abstractos ? Los métodos abstractos son métodos que no se implementan en la clase actual, de hecho no es posible hacerlo en la clase que los define, sino que hay que implementarlo en clases inferiores. Es importante tener en cuenta de que las clases que tengan métodos abstractos no se tienen que instancias. Esto es asà para evitar que el usuario llame a métodos no implementados. Si intentamos instanciar una clase con métodos abstractos el código se compilará correctamente pero al ejecutarlo obtendremos un error de ejecución.
La utilidad de las clases abstractas se basa en definir métodos que se implementarán en clases inferiores por fuerza, a menos que las queramos inutilizar para no poderlas instanciar. Los métodos abstractos tienen que ser virtuales por definición.
Volviendo a los ejemplos anteriores podÃamos haber declarado el método Ruedas de TVehiculo como abstracto y asà evitar la implementación extraña que devolvÃa -1. Para definir un método como abstracto hay que añadir la palabra abstract después de virtual.
type TVehiculo = class (TObject) public function Ruedas : integer; virtual; abstract; end;
Ahora hay que suprimir la implementación de TVehiculo.Ruedas. Ahora ya no es posible hacer instancias de TVehiculo pero si emplear variables de tipo TVehiculo con instancias de TMoto y TCoche que tienen el método Ruedas de tipo override. Hay que ir con cuidado con los métodos abstractos, si una clase no implementa algún método abstracto de la clase superior entonces la clase también será abstracta. Si la implementación en clases inferiores se hace estáticamente entonces el enlace se resolverá en tiempo de compilación : se intentará enlazar con el método abstracto y se obtendrá un error de ejecución. Nótese que los métodos descendientes directos de un método abstracto no pueden llamar al método inherited pues no está implementado. Sà es posible hacerlo en métodos que no sean descendientes inmediatos de métodos abstractos, incluso si en algún momento de la jerarquÃa el método ha sido abstracto. O sea, desde TCochecito podrÃamos llamar a inherited de Ruedas pero desde TCoche no.
14.5.Y el polimorfismo?
Ya lo hemos visto ocultado en los ejemplos anteriores. Cuando llamábamos Ruedas desde las distintas instancias hemos llamado al código adecuado gracias a los métodos virtuales.
La gracia es poder ejecutar código más concreto desde clases superiores. Por ejemplo, supongamos una clase TMusicPlayer. Esta clase implementa 4 métodos abstractos que son Play, Stop, Pause y Restart. El método Play hace sonar la música, el método Stop para la música, el método Pause pone el sistema musical en pausa y Restart lo reinicia.
Entonces podrÃamos implementar clases desciendentes de TMusicPlayer como por ejemplo TWavPlayer que harÃa sonar un archivo de sonido de tipo .WAV. O TMidiPlayer que hace sonar un archivo MIDI. TMP3Player que hace sonar un archivo MP3. Y todo simplemente conociendo 4 métodos que son Play, Stop, Pause y Restart. Cada clase los implementará como precise, todo dependerá de qué constructor haya hecho la instancia. Si lo hemos instanciado con TMP3Player se llamarán los métodos de TMP3Player, si lo hacemos con TMIDIPlayer se llamaran los métodos de TMIDIPlayer, etc.
Conceptos avanzados de POO
Métodos de clase
En C++ se les llama métodos static (no tiene nada que ver con los métodos virtual) y en Pascal se les llama métodos de clase. Un método de clase es un método que opera con clases y no con objetos tal como hemos visto con todos los métodos que hemos visto hasta ahora.
Hasta ahora, para poder emplear los métodos de un objeto necesitábamos instanciar primero la clase en una variable del tipo de la clase. Con los métodos de clase no es necesario ya que trabajan con la clase en sÃ. Esto quiere decir que los podemos llamar directamente sin haber tenido que instanciar la clase. El único caso que hemos visto hasta ahora era el método Create que se podÃa llamar directamente sin tener que instanciar la clase.
A diferencia de los métodos normales, los métodos de clase no pueden acceder a los campos de la clase (pues no tienen definida la variable Self). Tampoco pueden llamar a métodos que no sean métodos de clase ni mucho menos acceder a propiedades.
La utilidad de los métodos de clase es bastante restringida al no poder acceder a otros métodos que no sean también de clase ni a otras variables del objeto. A diferencia de otros lenguajes orientados a objetos como C++ o el Java, Pascal no incorpora un equivalente de "variables de clase" (o variables estáticas según la nomenclatura de C++ y Java) y por tanto la utilidad de los métodos de clase queda bastante reducida. Como sustituto de estas variables de clase, que en Pascal no existen, podemos emplear las variables de la sección implementation de una unit, ya que es posible declarar una clase en la sección interface de la unit e implementarla en la sección implementation.
Para declarar un método de clase añadiremos la palabra reservada class antes de la definición del método. Como a ejemplo vamos a ver un método de clase que nos indique cuantas instancias se han hecho de esta clase. Para hacerlo tendremos que sobreescribir el constructor y destructor de la clase . Recordemos que el constructor por defecto es Create y el destructor por defecto es Destroy. Es posible añadir más de un constructor o destructor (este último caso es muy poco habitual). Los constructores y destructores son métodos pro que se declaran con la palabra constructor y destructor respectivamente.
Los constructores suelen llevar parámetros que determinan algunos aspectos de la funcionalidad de la clase. También inicializan variables y hacen comprobaciones iniciales. Los destructores, a la contra, no suelen llevar parámetros (no tiene mucho sentido) y se encargan de liberar la memoria reservada en el constructor : punteros, otras clases, archivos abiertos, etc.
Es importante que el constructor antes de hacer nada, llame al constructor de la clase superior, que acabará por llamar al constructor de la clase TObject. Esto es asà para asegurarse que la reserva de memoria es correcta. En cambio, el constructor tiene que llamar al destructor de la clase superior una vez ha liberado sus datos, o sea, al final del método. Téngase en cuenta de que en la definición de TObject Destroy es virtual, de forma que tendremos que hacerlo override si queremos que todo funcione perfectamente.
Para implementar el ejemplo que hemos propuesto no hay más remedio que emplear una unit. Esto es asà porque emplearemos una variable que no queremos que accesible desde fuera de la clase y tampoco desde fuera de la unit.
En la sección interface declaramos la clase :
unit EjemploClase;
{$MODE OBJFPC}
interface
type
TContarInst = class (TObject)
public
constructor Create;
destructor Destroy; override;
class function ContarInstancias : Integer; // Método de clase
end;
En la sección implementation declararemos la que hará las veces de variable de clase, aunque no lo sea.
implementation var NumInstancias : Integer; // Hará las veces de "variable de clase"
Ya podemos implementar el constructor y el destructor. También inicializaremos la variable NumInstancias a cero, en la sección inicialización de la unit.
constructor TContarInst.Create; begin inherited Create; NumInstancias := NumInstancias + 1; end; destructor TContarInst.Destroy; begin NumInstancias := NumInstancias - 1; inherited Destroy; end; function TContarInst.ContarInstancias : Integer; begin ContarInstancias := NumInstancias; // Devolvemos la "variable de clase" end; initialization NumInstancias := 0; end.
Llegados aquà hay que hacer un comentario sobre inherited. Es posible que la clase ascendente implemente otros métodos con el mismo nombre y distintos parámetros, métodos sobrecargados. Para llamar al método adecuado es importante pasar bien los parámetros como si de una llamada normal se tratara pero precedida de inherited. En este caso no lo hemos hecho porque no lleva parámetros, incluso podrÃamos haber escrito simplemente inherited; pues el compilador ya entiende que el método que queremos llamar es el mismo que estamos implementando pero de la clase superior (Create y Destroy).
Vamos a ver el funcionamiento de esta clase que cuenta sus instancias.
program MetodosDeClase;
{$MODE OBJFPC}
uses EjemploClase;
var
C1, C2, C3 : TContarInst;
begin
Writeln('Instancias al empezar el programa : ',
TContarInst.ContarInstancias);
C1 := TContarInst.Create; // Creamos una instancia
Writeln('Instancias actuales : ', TContarInst.ContarInstancias);
C2 := TContarInst.Create;
Writeln('Instancias actuales : ', TContarInst.ContarInstancias);
C3 := TContarInst.Create;
Writeln('Instancias actuales : ', TContarInst.ContarInstancias);
C3.Free;
Writeln('Instancias actuales : ', TContarInst.ContarInstancias);
C2.Free;
Writeln('Instancias actuales : ', TContarInst.ContarInstancias);
C1.Free;
Writeln('Instancias al terminar el programa : ',
TContarInst.ContarInstancias);
end.
Tal como es de esperar el resultado del programa es :
Instancias al empezar el programa : 0 Instancias actuales : 1 Instancias actuales : 2 Instancias actuales : 3 Instancias actuales : 2 Instancias actuales : 1 Instancias al terminar el programa : 0
Invocación de métodos de clase
Tal como hemos visto, podemos llamar a un método de clase a través de el nombre de la clase, pero también mediante una instancia. Dentro de los métodos convencionales también podemos llamar a los métodos de clase.
Punteros a métodos
Es posible emplear un tipo especial de tipo procedimiento/función que podrÃamos llamar tipo de método. La declaración es idéntica a la de un tipo de procedimiento/función pero añadiendo las palabras of object al final de la declaración de tipo.
type TPunteroMetodo = procedure (Num : integer) of object;
Este tipo de método se puede emplear dentro de las clases como harÃamos con los tipos procedimiento/función normalmente. Es posible, como hemos visto, mediante tipos de procedimiento/función llamar diferentes funciones bajo un mismo nombre ya que las podemos asignar a funciones existentes. Los tipos de método tienen un comportamiento parecido. Declaramos la clase siguiente :
TImplementadorA = class (TObject) public procedure Ejecutar(Num : integer); end; TImplementadorB = class (TObject) public procedure Ejecutar(Num : integer); end;
Y una tercera clase donde emplearemos el puntero a método.
TEjemplo = class (TObject) public Operacion : TPunteroMetodo; end;
La implementación de las clases TImplementadorA y TImplementadorB es la siguiente :
procedure TImplementadorA.Ejecutar(Num : integer); begin Writeln(Num*7); end; procedure TImplementadorB.Ejecutar(Num : integer); begin Writeln(Num*Num); end;
Ahora es posible, dado que las declaraciones de Ejecutar ambas clases TImplementador es compatible con el tipo TPunteroMetodo asignar a Operacion los métodos Ejecutar de las clases TImplementadorA y TImplementadorB. Obsérvese el programa siguiente :
var
ImplA : TImplementadorA;
ImplB : TImplementadorB;
Ejemplo : TEjemplo;
begin
ImplA := TImplementadorA.Create;
ImplB := TImplementadorB.Create;
Ejemplo := TEjemplo.Create;
Ejemplo.Operacion := @ImplA.Ejecutar;
Ejemplo.Operacion(6); {42}
Ejemplo.Operacion := @ImplB.Ejecutar;
Ejemplo.Operacion(6); {36}
Ejemplo.Free;
ImplB.Free;
ImplA.Free;
end.
Esta asignación no habrÃa sido válida si TPunteroMetodo no fuera un tipo de método o también llamado puntero a método.
La utilidad de los punteros a método sirve cuando queremos modificar el comportamiento de un método. Téngase en cuenta que si el puntero a método no está asignado a algún otro método entonces al ejecutarlo se producirá un error de ejecución.
Paso de parámetros de tipo objeto
Podemos pasar parámetros de tipo objeto a funciones y procedimientos. En este caso habrá que ir con algo más de cuidado pues una clase no es una variable como las demás.
Las clases son siempre parámetros por referencia
Las clases se pasan siempre por referencia. Esto es asà ya que un objeto, o sea, una instancia de clase no es nada más que un puntero y por tanto no tiene sentido pasar una clase por valor. Esto implica que las modificaciones en la clase que hagamos en la función permanecerán después de la llamada.
Si la clase se pasa como un parámetro constante entonces no se podrán modificar variables directamente (mediante una asignación) pero aún es posible llamar métodos que modifiquen los campos del objeto. En caso que el parámetro sea var es lo mismo que si hubiéramos pasado un parámetro normal.
Las clases conviene que estén instanciadas
Es posible pasar una clase no instanciada e instanciarla dentro de la función pero no es muy recomendable pues nadie nos asegura de que ya ha sido instanciada. A la inversa tampoco, no libere clases dentro de funciones que hayan sido pasadas como parámetros.
Téngase en mente que los objetos son punteros
Por lo que no asigne un objeto a otro porqué no se copiarÃ