Introducción al lenguaje Pascal moderno para programadores
Mucha gente considera el lenguaje Pascal, como un lenguaje obsoleto, ya que en internet hay muchos recurso que hablan sobre el lenguaje Pascal, pero hablan sobre el «viejo» Pascal.
Michalis Kamburelis, creador del motor gráfico Castle Engine, escribió una guía muy concisa y sencilla en la que muestra las características de este lenguaje.
El texto original se puede encontrar aquí.
Contenido
- 1 ¿Por qué?
- 2 Básico
- 2.1 Hola Mundo
- 2.2 Funciones procedimientos y tipos primitivos
- 2.3 Testing (if)
- 2.4 Operadores lógicos, relacionales y bit a bit
- 2.5 Prueba de expresión única para valores múltiples (case)
- 2.6 Tipos y conjuntos enumerados, ordinales y matrices de longitud constante
- 2.7 Bucles (for, while, repeat, for .. in)
- 2.8 Salida, Logging
- 2.9 Convirtiendo a cadena
- 3 Units
- 4 Clases
- 5 Liberando las clases
- 6 Excepciones
- 7 Biblioteca en tiempo de ejecución (Run-time)
- 8 Funciones varias del lenguaje
- 8.1 Rutinas locales (anidadas)
- 8.2 Callbacks (también conocidos como eventos, también conocidos como punteros a funciones, también conocidos como variables de procedimiento)
- 8.3 Funciones anonimas (Anonymous functions)
- 8.4 Genéricos
- 8.5 Sobrecarga
- 8.6 Preprocesado
- 8.7 Registros
- 8.8 Objetos al estilo antiguo
- 8.9 Punteros
- 8.10 Sobrecarga de operadores
- 9 Características de las clases avanzadas
- 9.1 Privado y estrictamente privado
- 9.2 Más cosas dentro de clases y clases anidadas
- 9.3 Métodos de clase
- 9.4 Referencias de clase
- 9.5 Métodos de una clase estática
- 9.6 Propiedades de clase y variables
- 9.7 Ayudantes de clase (Helpers)
- 9.8 Constructores virtuales, destructores
- 9.9 Una excepción en el constructor.
- 10 Interfaces
- 11 10.2. CORBA and COM types of interfaces
- 12 Sobre este documento
¿Por qué?
Hay muchos libros y recursos sobre Pascal, pero muchos de ellos hablan sobre el antiguo Pascal, sin clases, unidades o genéricos. Así que escribí esta rápida introducción a lo que llamo Object Pascal moderno. La mayoría de los programadores que lo usan realmente no lo llaman «Objeto Pascal moderno», simplemente lo llamamos «nuestro Pascal». Pero al presentar el lenguaje, creo que es importante enfatizar que es un lenguaje moderno y orientado a objetos. Evolucionó mucho desde el viejo (Turbo) Pascal que mucha gente aprendió en las escuelas hace mucho tiempo. En cuanto a las funciones, es bastante similar a C++, Java o C#.
- Tiene todas las características modernas que espera: clases, unidades, interfaces, genéricos…
- Está compilado en un código nativo rápido.
- Es muy seguro.
- De alto nivel, pero también puede ser de bajo nivel si lo necesitas
También tiene un compilador excelente, portátil y de código abierto llamado Free Pascal, http://freepascal.org. Y un entorno de desarrollo integrado o IDE que lo acompaña (editor, depurador, una biblioteca de componentes visuales, diseñador de formularios) llamado Lazarus http://lazarus.freepascal.org/. Yo mismo, soy el creador de Castle Game Engine, https://castle-engine.io/, que es un motor de juegos 3D y 2D de código abierto que utiliza Pascal moderno para crear juegos en muchas plataformas (Windows, Linux, macOS, Android, iOS, Nintendo Switch; también viene WebGL).
Esta introducción está dirigida principalmente a programadores que ya tienen experiencia en otros lenguajes. No cubriremos aquí los significados de algunos conceptos universales, como «qué es una clase», solo mostraremos cómo hacerlo en Pascal.
Básico
Hola Mundo
1 2 3 4 5 6 |
{$mode objfpc}{$H+}{$J-} // Usa esta línea en todos los recursos program MyProgram; // guarda este programa como myprogram.lpr begin WriteLn('Hola Mundo!'); end. |
- Este es un programa completo que puede compilar y ejecutar. Este es un programa de línea de comandos, por lo que en cualquier caso — simplemente ejecute el ejecutable compilado desde la línea de comandos.
- Si usa la línea de comandos FPC, simplemente cree un nuevo archivo
myprogram.lpr
y ejecutefpc myprogram.lpr
- Si usas Lazarus, cree un nuevo proyecto (menú Proyecto → Nuevo proyecto → Programa Simple). Guárdelo como
myprogram
y pegue este código fuente como archivo principal. Compilar utilizando el elemento de menú Ejecutar → Compilar. - Este es un programa de línea de comandos, por lo que en cualquier caso, simplemente ejecute el ejecutable compilado desde la línea de comandos.
El resto de este artículo habla sobre el lenguaje Object Pascal, así que no esperes ver nada más elegante que la línea de comandos. Si quiere ver algo genial, simplemente cree un nuevo proyecto GUI en Lazarus (Proyecto → Nuevo proyecto → Aplicación). Listo — una aplicación GUI en funcionamiento, multiplataforma, con un aspecto nativo en todas partes, que utiliza una cómoda biblioteca de componentes visuales. El compilador Lazarus y Free Pascal vienen con muchas unidades listas para redes, GUI, base de datos, formatos de archivo (XML, json, imágenes…), subprocesos y todo lo que pueda necesitar. Ya mencioné mi genial Castle Game Engine antes 🙂
Funciones procedimientos y tipos primitivos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{$mode objfpc}{$H+}{$J-} program MyProgram; procedure MyProcedure(const A: Integer); begin WriteLn('A + 10 es: ', A + 10); end; function MyFunction(const S: string): string; begin Result := S+ 'las cadenas (strings) se gestionan automaticamente'; end; var X: Single; begin WriteLn(MyFunction('Note: ')); MyProcedure(5); // División usando "/" siempre devuelve un Float como resultado. Use "div" para dividir y obtener un integer X := 15 / 5; WriteLn('X is now: ', X); // notación científica WriteLn('X is now: ', X:1:2); // 2 decimales end. |
Para devolver un valor de una función, asigne algo a la variable mágica Result. Puede leer y configurar Result libremente, como una variable local.
1 2 3 4 5 6 |
function MyFunction(const S: string): string; begin Result := S + 'algo'; Result := Result + ' ¡algo más!'; Result := Result + ' ¡y más!'; end; |
También puede tratar el nombre de la función (como MyFunction
en el ejemplo anterior) como la variable a la que puede asignar datos. Pero lo desaconsejaría en el código nuevo, ya que parece «sospechoso» cuando se usa en el lado derecho de la expresión de asignación. Simplemente use Result siempre cuando quiera leer o configurar el resultado de la función.
1 2 3 4 5 6 7 8 9 |
function SumIntegersUntilZero: Integer; var I: Integer; begin Readln(I); Result := I; if I <> 0 then Result := Result + SumIntegersUntilZero(); end; |
Puede llamar a Exit
para finalizar la ejecución del procedimiento o función antes de que llegue al final final;. Si llama a Exit
sin parámetros en una función, devolverá lo último que configuró como Result. También puede usar la construcción Exit(X
), para establecer el resultado de la función y salir ahora — esto es como devolver la construcción X en lenguajes similares a C.
1 2 3 4 5 6 |
function AddName(const ExistingNames, NewName: string): string; begin if ExistingNames = '' then Exit(NewName); Result := ExistingNames + ', ' + NewName; end; |
Tenga en cuenta que el resultado de la función puede descartarse. Cualquier función puede usarse como un procedimiento. Esto tiene sentido si la función tiene algún efecto secundario (por ejemplo, modifica una variable global) además de calcular el resultado. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var Count: Integer; MyCount: Integer; function CountMe: Integer; begin Inc(Count); Result := Count; end; begin Count := 10; CountMe; // EL resultado de la función se descarta, pero la función es ejecutada, Count ahora es 11 MyCount := CountMe; // uso del resultado de la función, MyCount es igual a Count, el cual ahora es 12 end. |
Testing (if)
Use if .. then o if .. then .. else
para ejecutar algún código cuando se cumpla alguna condición. A diferencia de los lenguajes tipo C, en Pascal no tienes que envolver la condición entre paréntesis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var A: Integer; B: boolean; begin if A > 0 then DoSomething; if A > 0 then begin DoSomething; AndDoSomethingMore; end; if A > 10 then DoSomething else DoSomethingElse; // equivalente a lo de encima B := A > 10; if B then DoSomething else DoSomethingElse; end; |
El else
está emparejado con el último if
. Entonces esto funciona como esperas:
1 2 3 4 5 |
if A <> 0 then if B <> 0 then AIsNonzeroAndBToo else AIsNonzeroButBIsZero; |
Si bien el ejemplo con el IF
anidado anterior es correcto, a menudo es mejor colocar el IF anidado dentro de un bloque Begin… End
en tales casos. Esto hace que el código sea más obvio para el lector y seguirá siendo obvio incluso si estropeas la sangría. La versión mejorada del ejemplo se encuentra a continuación. Cuando agrega o elimina alguna otra cláusula en el código a continuación, es obvio a qué condición se aplicará (a la prueba A o la prueba B), por lo que es menos propenso a errores.
1 2 3 4 5 6 7 |
if A <> 0 then begin if B <> 0 then AIsNonzeroAndBToo else AIsNonzeroButBIsZero; end; |
Operadores lógicos, relacionales y bit a bit
Los operadores lógicos se llaman and, or, not, xor
. Su significado es probablemente obvio (busque «o exclusivo» si no está seguro de lo que hace xor
:)). Toman argumentos booleanos y devuelven un booleano. También pueden actuar como operadores bit a bit cuando ambos argumentos son valores enteros, en cuyo caso devuelven un número entero.
Los operadores relacionales (de comparación) son =, <>, >, <, <=, >=
. Si está acostumbrado a lenguajes similares a C, tenga en cuenta que en Pascal compara dos valores (verifique si son iguales) usando un solo carácter de igualdad A = B
(a diferencia de C donde usa A == B)
. El operador de asignación especial en Pascal es :=
.
Los operadores lógicos (o bit a bit) tienen mayor preferencia que los operadores relacionales. Es posible que deba usar paréntesis alrededor de algunas expresiones para tener el orden deseado de los cálculos.
Por ejemplo, esto es un error de compilación:
1 2 3 4 |
var A, B: Integer; begin if A = 0 and B <> 0 then ... // ejemplo incorrecto |
Lo anterior falla al compilar, porque el compilador primero quiere realizar un bit a bit y en el medio de la expresión: (0 and B)
. Esta es una operación bit a bit que devuelve un valor entero. Luego, el compilador aplica el operador = que produce un valor booleano A = (0 and B)
. Y finalmente, el error de «tipo no coincidente» surge después de intentar comparar el valor booleano A = (0 and B)
y el valor entero 0.
Esto es correcto:
1 2 3 4 |
var A, B: Integer; begin if (A = 0) and (B <> 0) then ... |
Se utiliza la evaluación de «cortocircuito». Considere esta expresión:
1 |
if MyFunction(X) and MyOtherFunction(Y) then... |
- Está garantizado que
MyFuncion(X)
se evaluará primero. - Y si
MyFunction(x)
devuelve falso, entonces la valor de la expresión será falso, yMyOtherFunction(Y)
no se ejecutará en absoluto. - De manera análoga es para el operador
OR
. Si se sabe que la expresión es verdadera (porque el primer operador es verdadero), el segundo operador no se evaluará.
Esto es particularmente útil cuando se escriben expresiones como:
1 |
if (A <> nil) and A.IsValid then... |
Esto funcionará bien, incluso cuando A es nulo. La palabra clave nil
es un puntero igual a cero (cuando se representa como un número). Se llama puntero nulo en muchos otros lenguajes de programación.
Prueba de expresión única para valores múltiples (case)
Si se debe ejecutar una acción diferente dependiendo del valor de alguna expresión, entonces la instrucción case .. of .. end
es útil.
1 2 3 4 5 6 7 8 9 10 11 |
case SomeValue of 0: DoSomething; 1: DoSomethingElse; 2: begin IfItsTwoThenDoThis; AndAlsoDoThis; end; 3..10: DoSomethingInCaseItsInThisRange; 11, 21, 31: AndDoSomethingForTheseSpecialValues; else DoSomethingInCaseOfUnexpectedValue; end; |
La cláusula else
es opcional (y corresponde a la predeterminada en lenguajes tipo C). Cuando ninguna condición coincide, y no hay más, entonces no pasa nada.
Si proviene de lenguajes similares a C y compara esto con la declaración de cambio en estos lenguajes, notará que no hay fallas automáticas. Esta es una bendición deliberada en Pascal. No es necesario que recuerde colocar las instrucciones de ruptura (Break
). En cada ejecución, como máximo se ejecuta una rama del caso, eso es todo.
Tipos y conjuntos enumerados, ordinales y matrices de longitud constante
El tipo enumerado en Pascal es un tipo muy agradable y opaco. Probablemente lo usará con mucha más frecuencia que las enumeraciones en otros lenguajes:)
1 2 |
type TAnimalKind = (akDuck, akCat, akDog); |
La convención es prefijar los nombres de enumeración con un atajo de dos letras de nombre de tipo, por lo tanto, ak = atajo para «Animal Kind». Esta es una convención útil, ya que los nombres de enumeración están en el espacio de nombres de la unidad (global). Entonces, al anteponerles el prefijo ak, minimiza las posibilidades de coincidencia con otros identificadores.
Las coincidencias en los nombres no son un problema. Es correcto que diferentes unidades definan el mismo identificador. Pero es una buena idea tratar de evitar las coincidencias de todos modos, para mantener el código simple de entender.
Puede evitar colocar nombres de enumeración en el espacio de nombres global mediante la directiva del compilador {$scopedenums on}
. Esto significa que tendrá que acceder a ellos calificados por un nombre de tipo, como TAnimalKind.akDuck.
La necesidad del prefijo ak desaparece en esta situación, y probablemente llamarás a las enumeraciones Pato, Gato, Perro. Esto es similar a las enumeraciones de C#.
El hecho de que el tipo enumerado sea opaco significa que no se puede asignar simplemente a un número entero. Sin embargo, para un uso especial, puede usar Ord(MyAnimalKind)
para convertir forzosamente enum
a int
, o typecast
TAnimalKind(MyInteger)
para convertir forzosamente int
en enum
. En el último caso, asegúrese de comprobar primero si MyInteger
está dentro del rango (0 a Ord(High(TAnimalKind)
Los tipos enumerados y ordinales se pueden usar como índices de matriz:
1 2 3 4 5 6 7 8 9 |
type TArrayOfTenStrings = array [0..9] of string; TArrayOfTenStrings1Based = array [1..10] of string; TMyNumber = 0..9; TAlsoArrayOfTenStrings = array [TMyNumber] of string; TAnimalKind = (akDuck, akCat, akDog); TAnimalNames = array [TAnimalKind] of string; |
1 |
También se pueden usar para crear conjuntos (un campo de bits internamente): |
También se pueden usar para crear conjuntos (un campo de bits internamente):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type TAnimalKind = (akDuck, akCat, akDog); TAnimals = set of TAnimalKind; var A: TAnimals; begin A := []; A := [akDuck, akCat]; A := A + [akDog]; A := A * [akCat, akDog]; Include(A, akDuck); Exclude(A, akDuck); end; |
Bucles (for, while, repeat, for .. in)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
{$mode objfpc}{$H+}{$J-} {$R+} // comprobación de rango activada - muy útil para depurar var MyArray: array [0..9] of Integer; I: Integer; begin // initialize for I := 0 to 9 do MyArray[I] := I * I; // show for I := 0 to 9 do WriteLn('Square is ', MyArray[I]); // does the same as above for I := Low(MyArray) to High(MyArray) do WriteLn('Square is ', MyArray[I]); // does the same as above I := 0; while I < 10 do begin WriteLn('Square is ', MyArray[I]); I := I + 1; // or "I += 1", or "Inc(I)" end; // does the same as above I := 0; repeat WriteLn('Square is ', MyArray[I]); Inc(I); until I = 10; //es lo mismo que lo anterior //Nota: Aquí enumero los valores de MyArray, no lo indices for I in MyArray do WriteLn('Square is ', I); end. |
Sobre los bucles repeat
y while
:
Hay dos diferencias entre estos tipos de bucle:
- La condición de bucle tiene un significado opuesto. En mientras.. le dices cuando continuar, pero en repetir.. hasta que le dices cuando parar.
- En caso de repetición, la condición no se comprueba al principio. Entonces, el ciclo de repetición siempre se ejecuta al menos una vez.
Sobre el bucle for I := …
:
El for I := .. to .. do
… lo construye de manera similar al bucle for tipo C. Sin embargo, está más restringido, ya que no puede especificar acciones/pruebas arbitrarias para controlar la iteración del bucle. Esto es estrictamente para iterar sobre números consecutivos (u otros tipos ordinales). La única flexibilidad que tiene es que puede usar downto
en lugar de to
para hacer que los números vayan hacia abajo.
A cambio, se ve limpio y está muy optimizado en ejecución. En particular, las expresiones para el límite inferior y superior solo se calculan una vez, antes de que comience el bucle.
Tenga en cuenta que el valor de la variable del contador del ciclo (I en este ejemplo) debe considerarse indefinido después de que finalice el ciclo, debido a posibles optimizaciones. Acceder al valor de I después del bucle puede provocar una advertencia del compilador. A menos que salga del ciclo prematuramente con Break
o Exit
: en tal caso, se garantiza que la variable contador conservará el último valor.
Sobre el bucle for I in …
El for I in .. do
.. es similar a la construcción foreach
en muchos lenguajes modernos. Funciona de forma inteligente en muchos tipos integrados:
- Puede iterar sobre todos los valores de la matriz (ejemplo anterior).
- Puede iterar sobre todos los valores posibles de un tipo enumerado:
1 2 3 4 |
var AK: TAnimalKind; begin for AK in TAnimalKind do... |
- Puede iterar sobre todos los elementos incluidos en el conjunto:
1 2 3 4 5 6 |
var Animals: TAnimals; AK: TAnimalKind; begin Animals := [akDog, akCat]; for AK in Animals do ... |
- Y funciona en tipos de listas personalizadas, genéricas o no, como TObjectList o TFPGObjectList.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, FGL; type TMyClass = class I, Square: Integer; end; TMyClassList = specialize TFPGObjectList<TMyClass>; var List: TMyClassList; C: TMyClass; I: Integer; begin List := TMyClassList.Create(true); // true = owns children try for I := 0 to 9 do begin C := TMyClass.Create; C.I := I; C.Square := I * I; List.Add(C); end; for C in List do WriteLn('Square of ', C.I, ' is ', C.Square); finally FreeAndNil(List); end; end. |
Todavía no explicamos el concepto de clases, por lo que el último ejemplo puede no ser obvio para usted todavía — solo continúe, tendrá sentido más adelante 🙂
Salida, Logging
Para simplemente generar cadenas en Pascal, use la rutina Write
o WriteLn
. Este último agrega automáticamente una nueva línea al final.
Esta es una rutina «mágica» en Pascal. Toma un número variable de argumentos y pueden ser de cualquier tipo. Todos se convierten en cadenas cuando se muestran, con una sintaxis especial para especificar el relleno y la precisión numérica.
1 2 3 4 |
WriteLn('¡Hola Mundo!'); WriteLn('Se puede mostrar un integer: ', 3 * 4); WriteLn('Puedes rellenar un integer: ', 666:10); WriteLn('Se puede mostrar un float: ', Pi:1:4); |
Para usar explícitamente una nueva línea en la cadena, use la constante LineEnding
(de FPC RTL). (El Castle Game Engine define también una constante NL más corta.) Las cadenas de Pascal no interpretan ninguna secuencia especial de barra invertida, por lo que escribir
1 |
WriteLn('Una línea.\Otra línea.'); // Ejemplo incorrecto |
no funciona como algunos de ustedes pensarían. Esto funcionará:
1 |
WriteLn('Una línea.' + LineEnding + 'Otra línea.'); |
1 |
o solo esto: |
1 2 |
WriteLn('Una línea.'); WriteLn('Otra línea.'); |
Tenga en cuenta que esto solo funcionará en aplicaciones de consola. Asegúrese de tener {$apptype CONSOLE}
(y no {$apptype GUI}
) definido en su archivo de programa principal. En algunos sistemas operativos en realidad no importa y funcionará siempre (Unix), pero en algunos sistemas operativos intentar escribir algo desde una aplicación GUI es un error (Windows).
En Castle Game Engine: use WriteLnLog
o WriteLnWarning
, nunca WriteLn
, para imprimir información de depuración. Siempre se dirigirán a alguna salida útil. En Unix, salida estándar. En la aplicación de GUI de Windows, archivo de registro. En Android, la función de registro de Android (visible cuando usa adb logcat). El uso de WriteLn
debe limitarse a los casos en los que escribe una aplicación de línea de comandos (como un convertidor/generador de modelos 3D) y sabe que la salida estándar está disponible.
Convirtiendo a cadena
Para convertir un número arbitrario de argumentos en una cadena (en lugar de generarlos directamente), tiene un par de opciones.
Puede convertir tipos particulares en cadenas usando funciones especializadas como IntToStr
y FloatToStr
. Además, puedes concatenar cadenas en Pascal simplemente agregándolas. Entonces puedes crear una cadena como esta: Mi número int es ' + IntToStr(MyInt) + ', y el valor de Pi es ' + FloatToStr(Pi)
.
- Ventaja: Absolutamente flexible. Hay muchas versiones sobrecargadas de XxxToStr y amigos (como
FormatFloat
), que cubren muchos tipos. La mayoría de ellos están en la unidadSysUtils
. - Otra ventaja: Consistente con las funciones inversas. Para convertir una cadena (por ejemplo, la entrada del usuario) de nuevo en un número entero o flotante, use StrToInt,
StrToFloat
y amigos (comoStrToIntDef
). - Desventaja: una concatenación larga de muchas llamadas y cadenas XxxToStr no se ve bien.
La función Format
, utilizada como Format('%d %f %s', [MyInt, MyFloat, MyString])
. Esto es como la función sprintf
en los lenguajes tipo C. Inserta los argumentos en los marcadores de posición en el patrón. Los marcadores de posición pueden usar una sintaxis especial para influir en el formato, p. %.4f
da como resultado un formato de punto flotante con 4 dígitos después del punto decimal.
- Ventaja: la separación de la cadena de patrón de los argumentos parece limpia. Si necesita cambiar la cadena del patrón sin tocar los argumentos (por ejemplo, al traducir), puede hacerlo fácilmente.
- Otra ventaja: no hay magia de compilación. Puede usar la misma sintaxis para pasar cualquier cantidad de argumentos de un tipo arbitrario en sus propias rutinas (declare el parámetro como una matriz de
const
). Luego puede pasar estos argumentos hacia abajo a Formato, o reconstruir la lista de parámetros y hacer lo que quiera con ellos. - Desventaja: el compilador no verifica si el patrón coincide con los argumentos. El uso de un tipo de marcador de posición incorrecto dará como resultado una excepción en tiempo de ejecución (excepción
EConvertError
, nada desagradable como un error de segmentación).
La rutina WriteStr(TargetString, …)
se comporta de manera similar a Write(…)
, excepto que el resultado se guarda en TargetString
.
- Ventaja: admite todas las características de
Write
, incluida la sintaxis especial para formatear como Pi: 1: 4. - Desventaja: la sintaxis especial para formatear es una «magia del compilador», implementada específicamente para rutinas como esta. Esto a veces es problemático, por ejemplo. no puede crear su propia rutina
MyStringFormatter (…)
que también permitiría la sintaxis especial como Pi: 1: 4. Por esta razón (y también porque no se implementó durante mucho tiempo en los principales compiladores de Pascal), esta construcción no es muy popular.
Units
Las unidades le permiten agrupar cosas comunes (cualquier cosa que se pueda declarar), para que las usen otras unidades y programas. Son equivalentes a módulos y paquetes en otros idiomas. Tienen una sección de interfaz, donde declara lo que está disponible para otras unidades y programas, y luego la implementación. Guarde la unidad MyUnit como myunit.pas
(en minúsculas con la extensión .pas).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$mode objfpc}{$H+}{$J-} unit MyUnit; interface procedure MyProcedure(const A: Integer); function MyFunction(const S: string): string; implementation procedure MyProcedure(const A: Integer); begin WriteLn('A + 10 es: ', A + 10); end; function MyFunction(const S: string): string; begin Result := S + 'strings sin gestionadas automaticamente'; end; end. |
Los programas finales se guardan como archivos myprogram.lpr
(lpr = archivo de programa Lazarus; en Delphi usaría .dpr). Tenga en cuenta que aquí son posibles otras convenciones, p. algunos proyectos solo usan .pas para el archivo de programa principal, algunos usan .pp para unidades o programas. Aconsejo usar .pas para unidades y .lpr para programas FPC/Lazarus.
Un programa puede utilizar una unidad mediante una palabra clave uses:
1 2 3 4 5 6 7 8 9 10 11 |
{$mode objfpc}{$H+}{$J-} program MyProgram; uses MyUnit; begin WriteLn(MyFunction('Note: ')); MyProcedure(5); end. |
Una unidad también puede contener secciones de inicialización y finalización. Este es el código que se ejecuta cuando el programa comienza y finaliza.
1 2 3 4 5 6 7 8 9 10 11 |
{$mode objfpc}{$H+}{$J-} unit initialization_finalization; interface implementation initialization WriteLn('¡Hola Mundo!'); finalization WriteLn('¡Adiós Mundo!'); end. |
Unidades que se llaman unas a otras
Una unidad también puede usar otra unidad. Se puede usar otra unidad en la sección de interfaz, o solo en la sección de implementación. El primero permite definir nuevos identificadores públicos (procedimientos, tipos…) además de los identificadores de otra unidad. Este último es más limitado (si usa una unidad solo en la sección de implementación, puede usar sus identificadores solo en la sección implementación).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{$mode objfpc}{$H+}{$J-} unit AnotherUnit; interface uses Classes; { El tipo "TComponent" (clase) es definida en la unidad Classes. Es es el motivo porque debemos poner la referencia a la unidad Classes al principio de todo } procedure DoSomethingWithComponent(var C: TComponent); implementation uses SysUtils; procedure DoSomethingWithComponent(var C: TComponent); begin { El procedimiento FreeAndNil se define en la unidad SysUtils. Ya que solo nos referimos a este procedimiento en la sección implementation, es correcto usar la unidad SysUtils en esta sección } FreeAndNil(C); end; end. |
No está permitido tener dependencias de unidades circulares en la interfaz. Es decir, dos unidades no pueden usarse entre sí en la sección interfaz. La razón es que para «comprender» la sección de interfaz de una unidad, el compilador primero debe «comprender» todas las unidades que utiliza en la sección de interfaz. El lenguaje Pascal sigue estrictamente esta regla y permite una compilación rápida y una detección completamente automática en el lado del compilador de las unidades que se deben volver a compilar. No hay necesidad de usar complicados archivos Makefile para una tarea simple de compilación en Pascal, y no hay necesidad de volver a compilar todo solo para asegurarse de que todas las dependencias se actualicen correctamente.
Se puede hacer una dependencia circular entre unidades cuando al menos una está declarada en la implementación. Por lo tanto, es correcto que la unidad A use la unidad B en la interfaz y luego la unidad B use la unidad A en la implementación.
Calificando identificadores con nombre de unidad
Diferentes unidades pueden definir el mismo identificador. Para mantener el código fácil de leer y buscar, por lo general debe evitarlo, aunque no siempre es posible. En tales casos, la última unidad en la cláusula uses «gana», lo que significa que los identificadores que introducen ocultan a los mismos identificadores introducidos por unidades anteriores.
Siempre puede definir explícitamente una unidad de un identificador dado, usándolo como MyUnit.MyIdentifier.
Esta es la solución habitual cuando el identificador que desea utilizar desde MyUnit
está oculto por otra unidad. Por supuesto, también puede reorganizar el orden de las unidades en su cláusula uses, aunque esto puede afectar otras declaraciones además de la que está tratando de arreglar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{$mode objfpc}{$H+}{$J-} program showcolor; //Ambas unidades, Graphics y GoogleMapsEngine define el tipo TColor; uses Graphics, GoogleMapsEngine; var {Esto funciona tal como esperamos. TColor hace referencia a la definición en y GoogleMapsEngine. } // Color: TColor; { Esto funciona } Color: Graphics.TColor; begin Color := clYellow; WriteLn(Red(Color), ' ', Green(Color), ' ', Blue(Color)); end. |
En el caso de las unidades, recuerda que tienen dos cláusulas Uses: una en la interfaz y otra en la implementación. La regla de que las unidades posteriores ocultan los elementos de las unidades anteriores se aplica aquí de forma coherente, lo que significa que las unidades utilizadas en la sección de implementación pueden ocultar los identificadores de las unidades utilizadas en la sección de interfaz. Sin embargo, recuerde que al leer la sección de la interfaz, solo importan las unidades utilizadas en la interfaz. Esto puede crear una situación confusa, donde el compilador considera diferentes dos declaraciones aparentemente iguales:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{$mode objfpc}{$H+}{$J-} unit UnitUsingColors; // Ejemplo incorrecto interface uses Graphics; procedure ShowColor(const Color: TColor); implementation uses GoogleMapsEngine; procedure ShowColor(const Color: TColor); begin // WriteLn(ColorToString(Color)); end; end. |
La unidad Graphics
(de Lazarus LCL) define el tipo de TColor
. Pero el compilador no podrá compilar la unidad anterior, alegando que no implementó un procedimiento ShowColor
que coincida con la declaración de la interfaz. El problema es que la unidad GoogleMapsEngine también define un tipo TColor
. Y se usa solo en la sección de implementación, por lo tanto, sombrea la definición de TColor
solo en la implementación. La versión equivalente de la unidad anterior, donde el error es obvio, se ve así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{$mode objfpc}{$H+}{$J-} unit UnitUsingColors; // Ejemplo incorrecto // Esto es lo que el compilador "ve" cuando intenta compilar el ejemplo anterior interface uses Graphics; procedure ShowColor(const Color: Graphics.TColor); implementation uses GoogleMapsEngine; procedure ShowColor(const Color: GoogleMapsEngine.TColor); begin // WriteLn(ColorToString(Color)); end; end. |
La solución es trivial en este caso, simplemente cambie en la implementación para usar explícitamente TColor
desde la unidad Graphics
. También podría solucionarlo moviendo GoogleMapsEngine
, a la sección de interfaz y antes que Graphics
, aunque esto podría tener otras consecuencias en casos reales, cuando UnitUsingColors
definiría más cosas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{$mode objfpc}{$H+}{$J-} unit UnitUsingColors; interface uses Graphics; procedure ShowColor(const Color: TColor); implementation uses GoogleMapsEngine; procedure ShowColor(const Color: Graphics.TColor); begin // WriteLn(ColorToString(Color)); end; end. |
Exponer los identificadores de una unidad de otra
A veces desea usar un identificador de una unidad y exponerlo en una nueva unidad. El resultado final debería ser que el uso de la nueva unidad hará que el identificador esté disponible en el espacio de nombres.
A veces, esto es necesario para preservar la compatibilidad con versiones anteriores de la unidad. A veces es bueno «ocultar» una unidad interna de esta manera.
Esto se puede hacer redefiniendo el identificador en su nueva unidad.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{$mode objfpc}{$H+}{$J-} unit MyUnit; interface uses Graphics; type { Expone TColor definido en la unidad Graphics como TMyColor. } TMyColor = TColor; { Como alternativa, se expone bajo el mismo nombre, pero haciendo referencia al nombre de la unidad. Podemos referirnos a nosotros mismos con la definición de TColor. } TColor = Graphics.TColor; const { Esto también funciona con constantes. } clYellow = Graphics.clYellow; clBlue = Graphics.clBlue; implementation end. |
Tenga en cuenta que este truco no se puede hacer tan fácilmente con procedimientos, funciones y variables globales. Con procedimientos y funciones, podría exponer un puntero constante a un procedimiento en otra unidad (consulte Devoluciones de llamada (también conocidas como eventos, también conocidas como punteros a funciones, también conocidas como variables de procedimiento), pero eso parece bastante enrevesado.
La solución habitual es entonces crear funciones triviales de «envoltura» que debajo simplemente llaman a las funciones desde la unidad interna, pasando los parámetros y devolviendo valores.
Para que esto funcione con variables globales, se pueden usar propiedades globales (a nivel de unidad), consulte Propiedad
Clases
Lo esencial
En el nivel básico, una clase es solo un contenedor para:
- Campos (que es un nombre elegante para «una variable dentro de una clase»)
- Métodos (que es un nombre elegante para «un procedimiento o función dentro de una clase»)
- Propiedades (que es una sintaxis elegante para algo que parece un campo, pero en realidad es un par de métodos para obtener y establecer algo; más en Propiedades)
1 2 3 4 5 6 7 8 9 10 11 |
type TMyClass = class MyInt: Integer; // Esto es un campo property MyIntProperty: Integer read MyInt write MyInt; // Esto es una propiedad procedure MyMethod; // Esto es un método end; procedure TMyClass.MyMethod; begin WriteLn(MyInt + 10); end; |
Herencia, is, as
Tenemos herencia y métodos virtuales.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
{$mode objfpc}{$H+}{$J-} program MyProgram; uses SysUtils; type TMyClass = class MyInt: Integer; procedure MyVirtualMethod; virtual; end; TMyClassDescendant = class(TMyClass) procedure MyVirtualMethod; override; end; procedure TMyClass.MyVirtualMethod; begin WriteLn('TMyClass muestra MyInt + 10: ', MyInt + 10); end; procedure TMyClassDescendant.MyVirtualMethod; begin WriteLn('TMyClassDescendant muestra MyInt + 20: ', MyInt + 20); end; var C: TMyClass; begin C := TMyClass.Create; try C.MyVirtualMethod; finally FreeAndNil(C); end; C := TMyClassDescendant.Create; try C.MyVirtualMethod; finally FreeAndNil(C); end; end. |
Por defecto los métodos no son virtuales, declararlos con virtual los hace virtuales. Las anulaciones deben estar marcadas con override
, de lo contrario recibirá una advertencia. Para ocultar un método sin anularlo (por lo general, no desea hacer esto, a menos que sepa lo que está haciendo), use reintroduce
.
Para probar la clase de una instancia en tiempo de ejecución, use el operador is. Para encasillar la instancia en una clase específica, use el operador as.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
{$mode objfpc}{$H+}{$J-} program is_as; uses SysUtils; type TMyClass = class procedure MyMethod; end; TMyClassDescendant = class(TMyClass) procedure MyMethodInDescendant; end; procedure TMyClass.MyMethod; begin WriteLn('MyMethod'); end; procedure TMyClassDescendant.MyMethodInDescendant; begin WriteLn('MyMethodInDescendant'); end; var Descendant: TMyClassDescendant; C: TMyClass; begin Descendant := TMyClassDescendant.Create; try Descendant.MyMethod; Descendant.MyMethodInDescendant; { Descendant has all functionality expected of the TMyClass, so this assignment is OK } C := Descendant; C.MyMethod; { this cannot work, since TMyClass doesn't define this method } //C.MyMethodInDescendant; if C is TMyClassDescendant then (C as TMyClassDescendant).MyMethodInDescendant; finally FreeAndNil(Descendant); end; end. |
En lugar de usar X como TMyClass
, también puede usar TMyClass(X)
encasillado sin marcar. Esto es más rápido, pero da como resultado un comportamiento indefinido si la X no es, de hecho, un descendiente de TMyClass
. Por lo tanto, no use el tipo de conversión TMyClass(X)
, o utilícelo solo en un código donde es absolutamente obvio que es correcto, por ejemplo, justo después de probar con is:
1 2 3 4 5 |
if A is TMyClass then (A as TMyClass).CallSomeMethodOfMyClass; // esto es más rápido if A is TMyClass then TMyClass(A).CallSomeMethodOfMyClass; |
Propiedades
Las propiedades son un muy buen «azúcar de sintaxis» para:
- Crear algo que parezca un campo (que se pueda leer y configurar) pero que debajo se realice llamando a métodos
getter
ysetter
. El uso típico es realizar algún efecto secundario (por ejemplo, volver a dibujar la pantalla) cada vez que cambia algún valor. - Crear algo que parezca un campo, pero que sea de solo lectura. En efecto, es como una función constante o sin parámetros
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
type TWebPage = class private FURL: string; FColor: TColor; function SetColor(const Value: TColor); public { No hay manrea de ajustar de manera directa. LLame al método Load, por ejemplo: Load('http://www.freepascal.org/'), para cargar una página y ajustar esta propiedad. } property URL: string read FURL; procedure Load(const AnURL: string); property Color: TColor read FColor write SetColor; end; procedure TWebPage.Load(const AnURL: string); begin FURL := AnURL; NetworkingComponent.LoadWebPage(AnURL); end; function TWebPage.SetColor(const Value: TColor); begin if FColor <> Value then begin FColor := Value; // Por ejemplo, produce la actulización cada vez que se cambian los valores. value changes Repaint; //Otro ejemplo, asegúrese de que alguna instancia subyacente, // como un "RenderingComponent" (sea lo que sea), // tiene un valor sincronizado de Color. RenderingComponent.Color := Value; end; end; |
Tenga en cuenta que en lugar de especificar un método, también puede especificar un campo (normalmente un campo privado) para obtener o establecer directamente. En el ejemplo anterior, la propiedad Color utiliza un método establecido SetColor
. Pero para obtener el valor, la propiedad Color se refiere directamente al campo privado FColor
. Hacer referencia directa a un campo es más rápido que implementar métodos getter
o setter
(más rápido para usted y más rápido en la ejecución).
Al declarar una propiedad se especifica:
- Si se puede leer y cómo (leyendo directamente un campo o usando un método «captador»).
- Y, de manera similar, si se puede establecer y cómo (escribiendo directamente en un campo designado o llamando a un método de «
setter
«).
El compilador verifica que los tipos y parámetros de los campos y métodos indicados coincidan con el tipo de propiedad. Por ejemplo, para leer una propiedad de número entero, debe proporcionar un campo de número entero o un método sin parámetros que devuelva un número entero.
Técnicamente, para el compilador, los métodos «getter
» y «setter
» son simplemente métodos normales y pueden hacer absolutamente cualquier cosa (incluidos los efectos secundarios o la aleatorización). Pero es una buena convención diseñar propiedades para que se comporten más o menos como campos:
- La función
getter
no debería tener efectos secundarios visibles (por ejemplo, no debería leer alguna entrada del archivo/teclado). Debe ser determinista (sin aleatorización, ni siquiera pseudoaleatorización :). Leer una propiedad muchas veces debería ser válido y devolver el mismo valor, si nada cambió.
Tenga en cuenta que está bien que getter
tenga algún efecto secundario invisible, por ejemplo, almacenar en caché un valor de algún cálculo (conocido por producir los mismos resultados para una instancia determinada), para devolverlo más rápido la próxima vez.
- De hecho, esta es una de las interesantes posibilidades de una función «
getter
«. La funciónsetter
siempre debe establecer el valor solicitado, de modo que llamar algetter
lo devuelva. No rechace valores inválidos en silencio en el «setter
» (provoque una excepción si es necesario). No convierta ni escale el valor solicitado. La idea es que después deMyClass.MyProperty := 123
; el programador puede esperar queMyClass.MyProperty = 123.
- Las propiedades de solo lectura a menudo se usan para hacer que algunos campos sean de solo lectura desde el exterior. Nuevamente, la buena convención es hacer que se comporte como una constante, al menos constante para esta instancia de objeto con este estado. El valor de la propiedad no debe cambiar inesperadamente. Conviértelo en una función, no en una propiedad, si su uso tiene un efecto secundario o devuelve algo aleatorio.
- El campo de «respaldo» de una propiedad es casi siempre privado, ya que la idea de una propiedad es encapsular todo acceso externo a ella.
- Es técnicamente posible crear propiedades de solo conjunto, pero aún no he visto un buen ejemplo de tal cosa 🙂
Las propiedades también se pueden definir fuera de la clase, a nivel de unidad. Entonces, tienen un propósito análogo: parecen una variable global, pero están respaldados por rutinas getter
y setter
.
Serializacion de propiedades
Las propiedades publicadas (Published
) son la base de una serialización (también conocida como componentes de transmisión) en Pascal. La serialización significa que los datos de la instancia se registran en un flujo (como un archivo), desde el cual se pueden restaurar más tarde.
La serialización es lo que sucede cuando Lazarus lee (o escribe) el estado del componente desde un archivo xxx.lfm. (En Delphi, el archivo equivalente tiene la extensión .dfm). También puede usar este mecanismo explícitamente, usando rutinas como ReadComponentFromTextStream
de la unidad LResources
. También puede usar otros algoritmos de serialización, por ejemplo la Unidad FpJsonRtti
(serializar a JSON).
En Castle Game Engine: use la unidad CastleComponentSerialize
(basada en FpJsonRtti
) para serializar nuestras jerarquías de interfaz de usuario y componentes de transformación.
En cada propiedad, puede declarar algunas cosas adicionales que serán útiles para cualquier algoritmo de serialización:
Puede especificar el valor predeterminado de la propiedad (usando la palabra clave predeterminada).
- Tenga en cuenta que aún debe inicializar la propiedad en el constructor a este valor predeterminado exacto (no se hace automáticamente). La declaración predeterminada es simplemente una información para el algoritmo de serialización: «cuando finaliza el constructor, la propiedad dada tiene el valor dado».
- Si la propiedad debe almacenarse en absoluto (usando la palabra clave almacenada).
Excepciones. Un ejemplo rápido
Tenemos excepciones. Se pueden atrapar con cláusulas try…except… end
, y finalmente tenemos secciones como try…finally…end
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
$mode objfpc}{$H+}{$J-} program MyProgram; uses SysUtils; type TMyClass = class procedure MyMethod; end; procedure TMyClass.MyMethod; begin if Random > 0.5 then raise Exception.Create('¡Elevando una excepción!'); end; var C: TMyClass; begin Randomize; C := TMyClass.Create; try C.MyMethod; finally FreeAndNil(C); end; end. |
Tenga en cuenta que la cláusula Finally
se ejecuta incluso si sale del bloque usando Exit
(desde la función/procedimiento/método) o Break
o Continue
(desde el cuerpo del bucle).
Consulte el capítulo Excepciones para obtener una descripción más detallada de las excepciones.
Especificadores de visibilidad
Como en la mayoría de los lenguajes orientados a objetos, tenemos especificadores de visibilidad para ocultar campos/métodos/propiedades.
Los niveles básicos de visibilidad son:
Public
todos pueden acceder a él, incluido el código en otras unidades.
Private
sólo accesible en esta clase.
Protected
solo accesible en esta clase y descendientes.
La explicación anterior de visibilidad privada y protegida no es precisamente cierta. El código en la misma unidad puede superar sus límites y acceder libremente a las cosas privadas y protegidas. A veces, esta es una buena característica, le permite implementar clases estrechamente conectadas. Use strict private
o strict protected
para asegurar sus clases de manera más estricta. Ver Private and strict private.
De forma predeterminada, si no especifica la visibilidad, la visibilidad de las cosas declaradas es pública. La excepción son las clases compiladas con {$M+}
o los descendientes de las clases compiladas con {$M+}
, que incluye todos los descendientes de TPersistent
, que también incluye todos los descendientes de TComponent (ya que TComponent desciende de TPersistent). Para ellos, se publica el especificador de visibilidad predeterminado, que es como público, pero además el sistema de transmisión sabe manejar esto.
No todos los campos y tipos de propiedades están permitidos en la sección publicada (no todos los tipos se pueden transmitir y solo las clases se pueden transmitir desde campos simples). Solo use público si no le importa la transmisión pero quiere algo disponible para todos los usuarios.
Antecesor por defecto
Si no declara el tipo antepasado, cada clase hereda de TObject
.
Self
La palabra clave especial Self
se puede usar dentro de la implementación de la clase para referirse explícitamente a su propia instancia. Es equivalente a esto de C++, Java y lenguajes similares.
Llamando al método heredado
Dentro de la implementación de un método, si llama a otro método, por defecto llama al método de su propia clase. En el código de ejemplo a continuación, TMyClass2.MyOtherMethod
llama a MyMethod
, que termina llamando a TMyClass2.MyMethod.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyClass1 = class procedure MyMethod; end; TMyClass2 = class(TMyClass1) procedure MyMethod; procedure MyOtherMethod; end; procedure TMyClass1.MyMethod; begin Writeln('TMyClass1.MyMethod'); end; procedure TMyClass2.MyMethod; begin Writeln('TMyClass2.MyMethod'); end; procedure TMyClass2.MyOtherMethod; begin MyMethod; // esto llama a TMyClass2.MyMethod end; var C: TMyClass2; begin C := TMyClass2.Create; try C.MyOtherMethod; finally FreeAndNil(C) end; end. |
Si el método no está definido en una clase determinada, llama al método de una clase antepasada. En efecto, cuando llama a MyMethod
en una instancia de TMyClass2, entonces:
- El compilador busca
TMyClass2.MyMethod
- Si no lo encuentra, busca
TMyClass1.MyMethod
. - Si no lo encuentra, busca
TObject.MyMethod
. - si no se encuentra, la compilación falla.
Puede probarlo comentando la definición de TMyClass2.MyMethod
en el ejemplo anterior. En efecto, TMyClass1.MyMethod
será llamado por TMyClass2.MyOtherMethod.
A veces, no desea llamar al método de su propia clase. Desea llamar al método de un antepasado (o al antepasado de un antepasado, etc.). Para hacer esto, agregue la palabra clave heredada antes de la llamada a MyMethod
, así:
1 |
inherited MyMethod; |
De esta forma, fuerza al compilador a comenzar a buscar desde una clase antecesora. En nuestro ejemplo, significa que el compilador está buscando MyMethod
dentro de TMyClass1.MyMethod
, luego TObject.MyMethod
y luego se da por vencido. Ni siquiera considera usar la implementación de TMyClass2.MyMethod
.
Consejo: a delante, cambie la implementación de TMyClass2.MyOtherMethod
anterior para usar MyMethod
heredado y vea la diferencia en el resultado.
La llamada heredada se usa a menudo para llamar al método antecesor del mismo nombre. De esta forma, los descendientes pueden mejorar a los ancestros (manteniendo la funcionalidad del ancestro, en lugar de reemplazar la funcionalidad del ancestro). Como en el ejemplo de abajo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyClass1 = class constructor Create; procedure MyMethod(const A: Integer); end; TMyClass2 = class(TMyClass1) constructor Create; procedure MyMethod(const A: Integer); end; constructor TMyClass1.Create; begin inherited Create; // esto llama a TObject.Create Writeln('TMyClass1.Create'); end; procedure TMyClass1.MyMethod(const A: Integer); begin Writeln('TMyClass1.MyMethod ', A); end; constructor TMyClass2.Create; begin inherited Create; // this calls TMyClass1.Create Writeln('TMyClass2.Create'); end; procedure TMyClass2.MyMethod(const A: Integer); begin inherited MyMethod(A); // this calls TMyClass1.MyMethod Writeln('TMyClass2.MyMethod ', A); end; var C: TMyClass2; begin C := TMyClass2.Create; try C.MyMethod(123); finally FreeAndNil(C) end; end. |
Dado que el uso inherited
para llamar a un método con el mismo nombre, con los mismos argumentos, es un caso muy frecuente, hay un atajo especial para ello: puede escribir inherited
; (palabra clave heredada seguida inmediatamente por un punto y coma, en lugar de un nombre de método). Esto significa «llamar a un método heredado con el mismo nombre, pasándole los mismos argumentos que el método actual».
Consejo: En el ejemplo anterior, todos los inherited
…; las llamadas podrían ser reemplazadas por un simple inherited
.
1 2 3 4 5 6 7 8 9 |
procedure TMyClass2.MyMethod(A: Integer); begin Writeln('TMyClass2.MyMethod beginning ', A); A := 456; { This calls TMyClass1.MyMethod with A = 456, regardless of the A value passed to this method (TMyClass2.MyMethod). } inherited; Writeln('TMyClass2.MyMethod ending ', A); end; |
Nota 2: por lo general, desea que MyMethod
sea virtual cuando muchas clases (a lo largo de la «cadena de herencia») lo definen. Más sobre los métodos virtuales en la sección a continuación. Pero la palabra clave heredada funciona independientemente de si el método es virtual o no. El heredado siempre significa que el compilador comienza a buscar el método en un ancestro, y tiene sentido tanto para los métodos virtuales como para los no virtuales.
Métodos virtuales, anular y reintroducir
Por defecto, los métodos no son virtuales. Esto es similar a C++ y diferente a Java.
Cuando un método no es virtual, el compilador determina a qué método llamar según el tipo de clase declarado actualmente, no según el tipo de clase realmente creado. La diferencia parece sutil, pero es importante cuando se declara que su variable tiene una clase como TFruit
, pero de hecho puede ser una clase descendiente como TApple
.
La idea de la programación orientada a objetos es que la clase descendiente siempre es tan buena como el antepasado, por lo que el compilador permite usar una clase descendiente siempre que se espera el antepasado. Cuando tu método no es virtual, esto puede tener consecuencias no deseadas. Considere el siguiente ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TFruit = class procedure Eat; end; TApple = class(TFruit) procedure Eat; end; procedure TFruit.Eat; begin Writeln('Comiendo fruta'); end; procedure TApple.Eat; begin Writeln('Comiendo un manzana'); end; procedure DoSomethingWithAFruit(const Fruit: TFruit); begin Writeln('We have a fruit with class ', Fruit.ClassName); Writeln('We eat it:'); Fruit.Eat; end; var Apple: TApple; // Nota: podrías declrar "Apple: TFruit" aquí begin Apple := TApple.Create; try DoSomethingWithAFruit(Apple); finally FreeAndNil(Apple) end; end. |
Este ejemplo mostrará
We have a fruit with class TApple
We eat it:
Eating a fruit
En efecto, la llamada Fruit.Eat
llamó a la implementación TFruit.Eat
y nada llamó a la implementación TApple.Eat
.
Si piensa en cómo funciona el compilador, esto es natural: cuando escribió Fruit.Eat
, se declaró que la variable Fruit
contenía una clase TFruit
. Entonces, el compilador estaba buscando el método llamado Eat
dentro de la clase TFruit
. Si la clase TFruit
no contuviera dicho método, el compilador buscaría dentro de un ancestro (TObject
en este caso). Pero el compilador no puede buscar dentro de los descendientes (como TApple
), ya que no sabe si la clase real de Fruit
es TApple
, TFruit
o algún otro descendiente de TFruit
(como TOrange
, que no se muestra en el ejemplo anterior).
En otras palabras, el método a llamar se determina en tiempo de compilación.
El uso de los métodos virtuales cambia este comportamiento. Si el método Eat
fuera virtual (a continuación se muestra un ejemplo), la implementación real que se llamará se determina en tiempo de ejecución. Si la variable Fruit
contiene una instancia de la clase TApple
(incluso si se declara como TFruit
), el método Eat
se buscará primero dentro de la clase TApple
.
En Object Pascal, para definir un método como virtual, necesita:
- Marque su primera definición (en el ancestro más alto) con la palabra clave virtual.
- Marque todas las demás definiciones (en los descendientes) con la palabra clave
override
. Todas las versiones anuladas deben tener exactamente los mismos parámetros (y devolver los mismos tipos, en el caso de las funciones).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TFruit = class procedure Eat; virtual; end; TApple = class(TFruit) procedure Eat; override; end; procedure TFruit.Eat; begin Writeln('Eating a fruit'); end; procedure TApple.Eat; begin Writeln('Eating an apple'); end; procedure DoSomethingWithAFruit(const Fruit: TFruit); begin Writeln('We have a fruit with class ', Fruit.ClassName); Writeln('We eat it:'); Fruit.Eat; end; var Apple: TApple; // Note: you could as well declare "Apple: TFruit" here begin Apple := TApple.Create; try DoSomethingWithAFruit(Apple); finally FreeAndNil(Apple) end; end. |
Este ejemplo mostrará
We have a fruit with class
TApple We eat it:
Eating an apple
Internamente, los métodos virtuales funcionan al tener la llamada tabla de métodos virtuales asociada con cada clase. Esta tabla es una lista de punteros a las implementaciones de métodos virtuales para esta clase. Al llamar al método Eat
, el compilador busca en una tabla de método virtual asociada con la clase real de Fruit
y usa un puntero a la implementación de Eat almacenada allí.
Si no usa la palabra clave override
, el compilador le advertirá que está ocultando (oscureciendo) el método virtual de un ancestro con una definición no virtual. Si está seguro de que esto es lo que desea, puede agregar una palabra clave de reintroducción. Pero en la mayoría de los casos, preferirá mantener el método virtual y agregar la palabra clave anular, asegurándose así de que siempre se invoque correctamente.
Liberando las clases
Recuerde liberar todas las instancias de las clases
Las instancias de clase deben liberarse manualmente, de lo contrario, se producen pérdidas de memoria. Recomiendo usar las opciones del compilador: -gl -gh,
para detectar fugas de memoria (ver https://castle-engine.io/manual_optimization.php#section_memory).
Tenga en cuenta que esto no se refiere a las excepciones planteadas. Aunque crees una clase cuando generas una excepción (y es una clase perfectamente normal, y también puedes crear tus propias clases para este propósito). Pero este la instancia de este tipo de clase se libera automáticamente. La instancia de este tipo de clase se libera automáticamente.
¿Cómo liberar?
Para liberar la instancia de una clase, es mejor llamar a FreeAndNil(A)
desde la unidad SysUtils
. Este procedimiento comprueba si A es nil
, si no, llama a su destructor y establece A en nil
. Entonces llamarlo muchas veces seguidas y no sería un error.
Es más o menos un atajo para
1 2 3 4 5 |
if A <> nil then begin A.Destroy; A := nil; end; |
En realidad, eso es una simplificación excesiva, ya que FreeAndNil
hace un truco útil y establece la variable A en nil
antes de llamar al destructor. Esto ayuda a prevenir cierta clase de errores — la idea es que el código «externo» nunca debería acceder a una instancia de la clase a medio destruir.
A menudo, también verá personas que usan el método A.Free
, que es como hacer
1 2 |
if A <> nil then A.Destroy; |
Esto libera A, a menos que sea nil
. Tenga en cuenta que, en circunstancias normales, nunca debe llamar a un método de una instancia que puede ser nil
. Entonces, la llamada A.Free
puede parecer sospechosa a primera vista, si A puede ser nil
. Sin embargo, el método Free
es una excepción a esta regla. Hace un truquillo en la implementación, es decir, comprueba si Self <> nil.
Nota: Este truquillo (permitir oficialmente que el método se use con Self = nil
) solo es posible para métodos no virtuales. Y siempre que Self = nil
sea posible, el método no puede llamar a ningún método virtual ni acceder a ningún campo, ya que estos se bloquearían con una violación de acceso cuando se llamara a un puntero nulo. Consulte el código de ejemplo method_with_self_nil.lpr. No recomendamos usar este truco en su propio código (para métodos virtuales o no virtuales), ya que es contrario a la intuición del uso normal; en general, todos los métodos de instancia deberían poder asumir que funcionan en una instancia válida (no nula). y puede acceder a los campos y llamar a cualquier otro método (virtual o no).
Aconsejo usar FreeAndNil(A)
siempre, sin excepciones, y nunca llamar directamente al método Free
o Destroy
destructor. Castle Game Engine lo hace así. Ayuda mantener una buena afirmación de que todas las referencias son nulas o apuntan a instancias válidas.
Liberación manual y automática
En muchas situaciones, la necesidad de liberar la instancia no es un gran problema. Simplemente escribe un destructor, que coincide con un constructor, y libere todo lo que se asignó en el constructor (o, más completamente, en toda la vida útil de la clase). Tenga cuidado de liberar cada cosa solo una vez. Por lo general, es una buena idea establecer la referencia liberada en nil
, por lo general, es más cómodo hacerlo llamando a FreeAndNil(A)
.
Entonces, sería algo así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
uses SysUtils; type TGun = class end; TPlayer = class Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end; constructor TPlayer.Create; begin inherited; Gun1 := TGun.Create; Gun2 := TGun.Create; end; destructor TPlayer.Destroy; begin FreeAndNil(Gun1); FreeAndNil(Gun2); inherited; end; |
Para evitar la necesidad de liberar explícitamente la instancia, también se puede usar la TComponent
como propietario. Un objeto que sea de su propiedad será liberado automáticamente por el propietario. El mecanismo es inteligente y nunca liberará una instancia ya liberada (por lo que las cosas también funcionarán correctamente si libera manualmente el objeto que posee antes). Podemos cambiar el ejemplo anterior por este:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
uses SysUtils, Classes; type TGun = class(TComponent) end; TPlayer = class(TComponent) Gun1, Gun2: TGun; constructor Create(AOwner: TComponent); override; end; constructor TPlayer.Create(AOwner: TComponent); begin inherited; Gun1 := TGun.Create(Self); Gun2 := TGun.Create(Self); end; |
Tenga en cuenta que necesitamos anular el constructor virtual de TComponent
en este caso. Entonces no podemos cambiar los parámetros del constructor. (En realidad, puede — declarar un nuevo constructor con overload. Pero tenga cuidado, ya que algunas funcionalidades, por ejemplo, la herencia, aún usarán el constructor virtual, así que asegúrese de que funcione correctamente en cualquier caso).
Tenga en cuenta que siempre puede usar un valor nulo para como propietario. De esta forma, el mecanismo de «propiedad» no se utilizará para este componente. Tiene sentido si necesita usar el descendiente de TComponent
, pero siempre que desee liberarlo manualmente. Para ello, crearía un componente descendiente como este: ManualGun := TGun.Create(nil);.
Otro mecanismo para la liberación automática es la funcionalidad OwnsObjects
(¡por defecto ya es verdad!) de clases como TFPGObjectList
o TObjectList
. Entonces podemos escribir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
uses SysUtils, Classes, FGL; type TGun = class end; TGunList = specialize TFPGObjectList<TGun>; TPlayer = class Guns: TGunList; Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end; constructor TPlayer.Create; begin inherited; //Actualmente el parámetro OwnsObjects por defecto vale true Guns := TGunList.Create(true); Gun1 := TGun.Create; Guns.Add(Gun1); Gun2 := TGun.Create; Guns.Add(Gun2); end; destructor TPlayer.Destroy; begin {Debemos tener cuidad al liberar la lista Ya que se liberara automaticamente su contenido} FreeAndNil(Guns); {No es necesario liberar Gun1, Gun2. Aunque es un buena hábito igualarlos a Nil De esta manera sabemos que están liberados. En esta simple clase, con un destructor sencillo es obvio que no vamos a acceder de nuevo. Pero hacer esto vale la pena en el caso de destructores más complejos. Como alternativa, podemos evitar declarar Gun1 y Gun2, y en su lugar usar Guna[0] y Gun[a1] en su propio código. O crear un método llamado Gun1 que devuelva Guns[0]. Gun1 := nil; Gun2 := nil; inherited; end; |
Tenga en cuenta que el mecanismo de «propiedad» de las clases de lista es simple y obtendrá un error si libera la instancia utilizando otros medios. Utilice el método Extract para eliminar algo de una lista sin liberarlo, asumiendo así la responsabilidad de liberarlo usted mismo.
En Castle Game Engine, los descendientes de TX3DNode tienen administración de memoria automática cuando se insertan como hijos de otro TX3DNode. El nodo raíz X3D, TX3DRootNode, a su vez, suele ser propiedad de TCastleSceneCore
. Algunas otras elementos también tienen un mecanismo de propiedad simple: busque parámetros y propiedades llamados OwnsXxx.
El destructor virtual llamado Destroy
Como viste en los ejemplos anteriores, cuando se destruye la clase, se llama a su destructor llamado Destroy
.
En teoría, podría tener múltiples destructores, pero en la práctica casi nunca es una buena idea. Es mucho más fácil tener un solo destructor llamado Destroy
, que a su vez es llamado por el método Free
, que a su vez es llamado por el procedimiento FreeAndNil
.
El destructor Destroy
en TObject
se define como un método virtual, por lo que siempre debe marcarlo con la palabra clave override
en todas sus clases (ya que todas las clases descienden de TObject
). Esto hace que el método Free
funcione correctamente. Recuerde cómo funcionan los métodos virtuales a partir de métodos virtuales, anular y volver a introducir.
Nota: Esta información sobre los destructores es, de hecho, inconsistente con los constructores.
Es normal que una clase tenga múltiples constructores. Por lo general, todos se llaman Create
y solo tienen diferentes parámetros, pero también está bien inventar otros nombres para los constructores.
Además, el constructor Create
en TObject
no es virtual, por lo que no lo marca con anulado en los descendientes.
Todo esto le brinda un poco de flexibilidad adicional al definir constructores. A menudo no es necesario hacerlos virtuales, por lo que, de forma predeterminada, no está obligado a hacerlo.
Tenga en cuenta, sin embargo, que esto cambia para los descendientes de TComponent
. El TComponent
define un constructor virtual Create(AOwner: TComponent)
. Necesita un constructor virtual para que funcione el sistema de herencia. Al definir los descendientes del TComponent
, debe anular este constructor (y marcarlo con la palabra clave override
) y realizar toda su inicialización dentro de él. También está bien definir constructores adicionales, pero solo deben actuar como «ayudantes». La instancia siempre debería funcionar cuando se crea con el constructor Create(AOwner: TComponent)
; de lo contrario, no se construirá correctamente durante la transmisión (streaming). La transmisión utiliza, por ejemplo al guardar y cargar este componente en un formulario de Lazarus.
Notificación de liberación
Si copia una referencia a la instancia, de modo que tiene dos referencias a la misma en memoria, y luego una de ellas se libera, la otra se convierte en un «puntero huerfano». No se debe acceder a este, ya que apunta a una memoria que ya no está asignada. Acceder a él puede resultar en un error de tiempo de ejecución, o que se devuelva basura (ya que la memoria puede reutilizarse para otras cosas en su programa).
Usar FreeAndNil
para liberar la instancia no ayuda aquí. FreeAndNil
establece en cero solo la referencia que obtuvo, no hay forma de que establezca todas las demás referencias automáticamente. Considere este código:
1 2 3 4 5 6 7 8 9 |
var Obj1, Obj2: TObject; begin Obj1 := TObject.Create; Obj2 := Obj1; FreeAndNil(Obj1); // what happens if we access Obj1 or Obj2 here? end; |
Al final de este bloque, el Obj1
es nulo. Si algún código tiene que acceder a él, puede usar de manera confiable si Obj1 <> nil
entonces… para evitar llamar a métodos en una instancia liberada, como
1 2 |
if Obj1 <> nil then WriteLn(Obj1.ClassName); |
Intentar acceder a un campo de una instancia nula da como resultado una excepción predecible en tiempo de ejecución.
Entonces, incluso si algún código no verificará Obj1 <> nil
, y accederá ciegamente al campo Obj1
, obtendrá una excepción en tiempo de ejecución. Lo mismo ocurre con llamar a un método virtual o llamar a un método no virtual que accedió a un campo de una instancia nula.
Con Obj2
, las cosas son menos predecibles. No es nulo, pero no es válido. Intentar acceder a un campo de una instancia no válida no nula da como resultado un comportamiento impredecible — tal vez una excepción de violación de acceso, tal vez una devolución de datos basura.
Hay varias soluciones para ello:
- Una solución es tener buen cuidado y leer la documentación. No suponga nada sobre la vida útil de la referencia, si se crea mediante otro código. Si una clase
TCar
tiene un campo que apunta a alguna instancia deTWheel
, es una convención que la referencia a la rueda sea válida mientras exista la referencia al automóvil, y el automóvil liberará sus ruedas dentro de su destructor. Pero eso es solo una convención, la documentación debe mencionar si está sucediendo algo más complicado. - En el ejemplo anterior, justo después de liberar la instancia de
Obj1
, simplemente puede configurar la variableObj2
explícitamente ennil
. Eso es trivial en este caso simple. - La solución más elegante es el mecanismo de «notificación» de la clase
TComponent
. Se puede notificar a un componente cuando se libera otro componente y, por lo tanto, establecer su referencia ennil
. - Por lo tanto, obtienes algo así como una referencia débil. Puede hacer frente a varios escenarios de uso, por ejemplo, puede dejar que el código externo a la clase establezca su referencia, y el código externo también puede liberar la instancia en cualquier momento.
- Esto requiere que ambas clases desciendan de
TComponent
. Usarlo en general se reduce a llamar aFreeNotification
,RemoveFreeNotification
y anular Notificación. - Esto requiere que ambas clases desciendan de
TComponent
. Usarlo en general se reduce a llamar aFreeNotification
,RemoveFreeNotification
y anular Notificación.
Aquí hay un ejemplo completo que muestra cómo usar este mecanismo, junto con el constructor/destructor y una propiedad setter
. A veces se puede hacer más simple, pero esta es la versión completa que siempre es correcta 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
type TControl = class(TComponent) end; TContainer = class(TComponent) private FSomeSpecialControl: TControl; procedure SetSomeSpecialControl(const Value: TControl); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; public destructor Destroy; override; property SomeSpecialControl: TControl read FSomeSpecialControl write SetSomeSpecialControl; end; implementation procedure TContainer.Notification(AComponent: TComponent; Operation: TOperation); begin inherited; if (Operation = opRemove) and (AComponent = FSomeSpecialControl) then { Se iguala a Nil por SetSomeSpecialControl para hacer una correcta liberación } SomeSpecialControl := nil; end; procedure TContainer.SetSomeSpecialControl(const Value: TControl); begin if FSomeSpecialControl <> Value then begin if FSomeSpecialControl <> nil then FSomeSpecialControl.RemoveFreeNotification(Self); FSomeSpecialControl := Value; if FSomeSpecialControl <> nil then FSomeSpecialControl.FreeNotification(Self); end; end; destructor TContainer.Destroy; begin { Se iguala a Nil por SetSomeSpecialControl, para desactivar la notificación de liberación } SomeSpecialControl := nil; inherited; end; |
Observador de notificaciones (Castle Game Engine)
En Castle Game Engine recomendamos usar TFreeNotificationObserver
de la unidad CastleClassUtils
en lugar de llamar directamente a FreeNotification
, RemoveFreeNotification
y anular Notificación.
En general, usar TFreeNotificationObserver
parece un poco más simple que usar el mecanismo FreeNotification
directamente (aunque admito que es cuestión de gustos). Pero, en particular, cuando se debe observar la misma instancia de clase por múltiples razones, TFreeNotificationObserver es mucho más fácil de usar (el uso directo de FreeNotification en este caso puede complicarse, ya que debe estar atento para no cancelar el registro de la notificación demasiado pronto).
Este es el código de ejemplo que usa TFreeNotificationObserver
, para lograr el mismo efecto que el ejemplo en la sección anterior:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
type TControl = class(TComponent) end; TContainer = class(TComponent) private FSomeSpecialControlObserver: TFreeNotificationObserver; FSomeSpecialControl: TControl; procedure SetSomeSpecialControl(const Value: TControl); procedure SomeSpecialControlFreeNotification(const Sender: TFreeNotificationObserver); public constructor Create(AOwner: TComponent); override; property SomeSpecialControl: TControl read FSomeSpecialControl write SetSomeSpecialControl; end; implementation uses CastleComponentSerialize; constructor TContainer.Create(AOwner: TComponent); begin inherited; FSomeSpecialControlObserver := TFreeNotificationObserver.Create(Self); FSomeSpecialControlObserver.OnFreeNotification := {$ifdef FPC}@{$endif} SomeSpecialControlFreeNotification; end; procedure TContainer.SetSomeSpecialControl(const Value: TControl); begin if FSomeSpecialControl <> Value then begin FSomeSpecialControl := Value; FSomeSpecialControlObserver.Observed := Value; end; end; procedure TContainer.SomeSpecialControlFreeNotification(const Sender: TFreeNotificationObserver); begin // Igualar la propiedad a Nil, cuando el componente de referencia es liberado SomeSpecialControl := nil; end; |
Ver https://castle-engine.io/custom_components
Excepciones
Descripción general
Las excepciones permiten interrumpir la ejecución normal del código.
- En cualquier punto dentro del programa, puede generar una excepción utilizando la palabra clave raise.En efecto, las líneas de código que siguen a la llamada raise… no se ejecutarán
- Se puede capturar una excepción usando una construcción try…except…end. Detectar una excepción significa que de alguna manera «lidias» con la excepción, y el siguiente código debería ejecutarse como de costumbre, la excepción ya no se propaga hacia arriba.
- Nota: Si se genera una excepción pero nunca se detecta, hará que toda la aplicación se detenga con un error. Pero en las aplicaciones LCL, las excepciones siempre se detectan alrededor de los eventos (y provocan el cuadro de diálogo LCL) si no las detecta antes.
- En las aplicaciones de Castle Game Engine que usan CastleWindow, de manera similar, siempre detectamos excepciones en torno a sus eventos (y mostramos el cuadro de diálogo adecuado).
- Por lo tanto, no es tan fácil hacer una excepción que no se detecte en ninguna parte (no se detecte en su código, código LCL, código CGE…).
- Si bien una excepción interrumpe la ejecución, puede usar la construcción try…finally…end para ejecutar algún código siempre, incluso si el código fue interrumpido por una excepción.
- La construcción try…finally…end también funciona cuando el código es interrumpido por las palabras clave Break, Continue o Exit. El objetivo es ejecutar siempre el código en la sección finalmente.
Una «excepción» es, en general, es una instancia de una clase.
- El compilador no impone ninguna clase en particular. Solo debe llamar a raise XXX donde XXX es una instancia de cualquier clase. Cualquier clase (por lo tanto, cualquier cosa que descienda de TObject) será correcto.
- Es una convención estándar que las clases de excepción desciendan de una clase de excepción especial. La clase Exception extiende TObject, agregando una propiedad Message de cadena y un constructor para configurar fácilmente esta propiedad. Todas las excepciones generadas por la biblioteca estándar descienden de Exception. Recomendamos seguir esta convención.
- Las clases de excepción (por convención) tienen nombres que comienzan con E, no con T. Como ESomethingBadHappened.
- El compilador liberará automáticamente el objeto de excepción cuando se maneje. No lo liberes tú mismo.
- En la mayoría de los casos, simplemente construyes el objeto al mismo tiempo que llamas a raise, como raise EAlgoMalOcurrido.Crear(‘Descripción de lo malo que sucedió’).
Levantamiento, elevación, propagación (Raising)
Si desea generar su propia excepción, declararla y llame a la elevación (raise)… cuando corresponda:
1 2 3 4 5 6 7 8 9 |
type EInvalidParameter = class(Exception); function ReadParameter: String; begin Result := Readln; if Pos(' ', Result) <> 0 then raise EInvalidParameter.Create('Invalid parameter, space is not allowed'); end; |
Tenga en cuenta que la expresión que sigue a la elevación debe ser una instancia de clase válida para elevar. Casi siempre creará la instancia de excepción aquí.
También puede usar el constructor CreateFmt, que es un atajo cómodo para Create(Format(MessageFormat, MessageArguments))
. Esta es una forma común de proporcionar más información al mensaje de excepción. Podemos mejorar el ejemplo anterior así:
1 2 3 4 5 6 7 8 9 |
type EInvalidParameter = class(Exception); function ReadParameter: String; begin Result := Readln; if Pos(' ', Result) <> 0 then raise EInvalidParameter.CreateFmt('Parámetro invalido %s, espacio no está permitido', [Result]); end; |
Atrapando
Puede detectar una excepción como esta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var Parameter1, Parameter2, Parameter3: String; begin try Writeln('Input 1st parameter:'); Parameter1 := ReadParameter; Writeln('Input 2nd parameter:'); Parameter2 := ReadParameter; Writeln('Input 3rd parameter:'); Parameter3 := ReadParameter; except //Captura EInvalidParameter elevada por alguno de las líneas que llaman a ReadParametes on EInvalidParameter do Writeln('Ocurrió la excepción EInvalidParameter'); end; end; |
Para mejorar el ejemplo anterior, podemos declarar el nombre de la instancia de excepción (usaremos E en el ejemplo). De esta manera podemos imprimir el mensaje de excepción:
1 2 3 4 5 6 |
try ... except on E: EInvalidParameter do Writeln('Ocurrió la excepción EInvalidParameter con el mensaje: ' + E.Message); end; |
O también podemos comprobar clases de excepciones
1 2 3 4 5 6 7 8 |
try ... except on E: EInvalidParameter do Writeln('Ocurrió la excepción EInvalidParameter con el mensaje: ' + E.Message); on E: ESomeOtherException do Writeln('Ocurrió la excepción ESomeOtherException con el mensaje:: ' + E.Message); end; |
También puede reaccionar a cualquier excepción generada, si no usa ninguna expresión:
1 2 3 4 5 6 7 |
try ... except Writeln('Atención: ocurrió alguna excepción'); end; //ATENCIÓN: NO SIGAS ESTE EJEMPLO SIN LEER EL AVISO MÁS ABAJO //SOBRE COMO CAPTURAR TODAS LAS EXCEPCIONES |
En general, solo debe capturar excepciones de una clase específica, que señalan un problema particular con el que sabe qué hacer. Tenga cuidado con la captura de excepciones de tipo general (como la captura de cualquier excepción o cualquier TObject), ya que puede capturar fácilmente demasiadas y luego causar problemas al depurar otros problemas.
- ¿La excepción indica un problema en la entrada del usuario?
- Entonces deberías reportarlo al usuario.
- ¿La excepción indica un error en su código? Luego, debe corregir el código para evitar que ocurra la excepción.
Otra forma de capturar todas las excepciones es usar:
1 2 3 4 5 6 7 |
try ... except Writeln('Atención: ocurrió alguna excepción'); end; //ATENCIÓN: NO SIGAS ESTE EJEMPLO SIN LEER EL AVISO MÁS ABAJO //SOBRE COMO CAPTURAR TODAS LAS EXCEPCIONES |
Aunque normalmente es suficiente para capturar Exception
:
1 2 3 4 5 6 7 8 |
try ... except on E: Exception do Writeln('Atención, ocurrió una excepción: ' + E.ClassName + ', message: ' + E.Message); end; //ATENCIÓN: NO SIGAS ESTE EJEMPLO SIN LEER EL AVISO MÁS ABAJO //SOBRE COMO CAPTURAR TODAS LAS EXCEPCIONES |
Puede «volver a generar» la excepción en el bloque except… finally
, si así lo decide. Puede hacer aumentar E si la instancia de excepción es E, también puede usar aumentar sin parámetros. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 |
try ... except on E: EInvalidSoundFile do begin if E.InvalidUrl = 'http://example.com/blablah.wav' then Writeln('ATención: http://example.com/blablah.wav falló la carga, ignorelo') else raise; end; end; |
Tenga en cuenta que, aunque la excepción es una instancia de un objeto, nunca debe liberarlo manualmente después de generarlo. El compilador generará el código adecuado que se asegura de liberar el objeto de excepción una vez que se maneja.
Finalmente (hacer cosas sin importar si ocurrió una excepción)
A menudo usas try
.. La forma de escribirlo se ve así:
1 2 3 4 5 6 7 8 9 10 11 12 |
procedure MyProcedure; var MyInstance: TMyClass; begin MyInstance := TMyClass.Create; try MyInstance.DoSomething; MyInstance.DoSomethingElse; finally FreeAndNil(MyInstance); end; end; |
Esto siempre funciona de manera confiable y no provoca pérdidas de memoria, incluso si MyInstance.DoSomething
o MyInstance.DoSomethingElse
generan una excepción.
Tenga en cuenta que esto tiene en cuenta que las variables locales, como MyInstance
arriba, tienen valores indefinidos (pueden contener «basura de memoria» aleatoria) antes de la primera asignación. Es decir, escribir algo como esto no sería válido:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Ejemplo incorrecto procedure MyProcedure; var MyInstance: TMyClass; begin try CallSomeOtherProcedure; MyInstance := TMyClass.Create; MyInstance.DoSomething; MyInstance.DoSomethingElse; finally FreeAndNil(MyInstance); end; end; |
El ejemplo anterior no es válido: si ocurre una excepción dentro de TMyClass.Create
(un constructor también puede generar una excepción), o dentro de CallSomeOtherProcedure, entonces la variable MyInstance
no se inicializa. Llamar a FreeAndNil(MyInstance)
intentará llamar al destructor de MyInstance
, que probablemente se bloquee con una infracción de acceso (falla de segmentación). En efecto, una excepción provoca otra excepción, lo que hará que el informe de errores no sea muy útil: no verá el mensaje de la excepción original.
A veces se justifica arreglar el código anterior inicializando primero todas las variables locales a cero (en las que llamar a FreeAndNil
es seguro y no hará nada). Esto tiene sentido si libera muchas instancias de clase. Así que los dos ejemplos de código a continuación funcionan igual de bien:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
procedure MyProcedure; var MyInstance1: TMyClass1; MyInstance2: TMyClass2; MyInstance3: TMyClass3; begin MyInstance1 := TMyClass1.Create; try MyInstance1.DoSomething; MyInstance2 := TMyClass2.Create; try MyInstance2.DoSomethingElse; MyInstance3 := TMyClass3.Create; try MyInstance3.DoYetAnotherThing; finally FreeAndNil(MyInstance3); end; finally FreeAndNil(MyInstance2); end; finally FreeAndNil(MyInstance1); end; end; |
Probablemente sea más legible de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
procedure MyProcedure; var MyInstance1: TMyClass1; MyInstance2: TMyClass2; MyInstance3: TMyClass3; begin MyInstance1 := nil; MyInstance2 := nil; MyInstance3 := nil; try MyInstance1 := TMyClass1.Create; MyInstance1.DoSomething; MyInstance2 := TMyClass2.Create; MyInstance2.DoSomethingElse; MyInstance3 := TMyClass3.Create; MyInstance3.DoYetAnotherThing; finally FreeAndNil(MyInstance3); FreeAndNil(MyInstance2); FreeAndNil(MyInstance1); end; end; |
Nota:
En este ejemplo simple, también podría presentar un argumento válido de que el código debe dividirse en 3 procedimientos separados, uno llamándose entre sí.
Cómo se muestran las excepciones en varias bibliotecas
En el caso de Lazarus LCL, las excepciones generadas durante los eventos (varias devoluciones de llamada asignadas a las propiedades OnXxx de los componentes LCL) se capturarán y darán como resultado un agradable mensaje de diálogo que permite al usuario continuar y detener la aplicación. Esto significa que sus propias excepciones no «salen» de Application.ProcessMessages
, por lo que no rompen automáticamente la aplicación. Puede configurar lo que sucede usando TApplicationProperties.OnException
. De manera similar, en el caso de Castle Game Engine con CastleWindow: la excepción se captura internamente y da como resultado un buen mensaje de error. Por lo tanto, las excepciones no «salen» de Application.ProcessMessages
. Nuevamente, puede configurar lo que sucede usando Application.OnException
.
Algunas otras bibliotecas GUI pueden hacer algo similar a lo anterior.
En el caso de otras aplicaciones, puede configurar cómo se muestra la excepción asignando una devolución de llamada global a OnHaltProgram
.
Biblioteca en tiempo de ejecución (Run-time)
Input/output using streams
Los programas modernos deberían usar la clase TStream
y sus muchos descendientes para hacer entrada/salida. Tiene muchos descendientes útiles, como TFileStream
, TMemoryStream
, TStringStream
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Classes; var S: TStream; InputInt, OutputInt: Integer; begin InputInt := 666; S := TFileStream.Create('my_binary_file.data', fmCreate); try S.WriteBuffer(InputInt, SizeOf(InputInt)); finally FreeAndNil(S); end; S := TFileStream.Create('my_binary_file.data', fmOpenRead); try S.ReadBuffer(OutputInt, SizeOf(OutputInt)); finally FreeAndNil(S); end; WriteLn('Se obtuvo un entero de la lectura del fichero: ', OutputInt); end. |
En Castle Game Engine: debe usar la función de descarga para crear una transmisión que obtenga datos de cualquier URL. Los archivos regulares, los recursos HTTP y HTTPS, los activos de Android y son compatibles de esta manera. Además, para acceder el recurso dentro de los datos de tu juego (en el subdirectorio de datos) usa la URL especial castle-data:/xxx. Ejemplos:
1 2 |
EnableNetwork := true; S := Download('https://castle-engine.io/latest.zip'); |
1 |
S := Download('file:///home/michalis/my_binary_file.data'); |
1 |
S := Download('castle-data:/gui/my_image.png'); |
Para leer archivos de texto, recomendamos utilizar la clase TTextReader. Proporciona una API orientada a la línea y envuelve un TStream en su interior. El constructor TTextReader puede tomar una URL, o puede pasar allí su fuente TStream personalizada.
1 2 3 4 5 6 7 |
Text := TTextReader.Create('castle-data:/my_data.txt'); try while not Text.Eof do WriteLnLog('NextLine', Text.ReadLn); finally FreeAndNil(Text); end; |
Contenedores (Listas, diccionarios) usando genéricos
La biblioteca de idiomas y tiempo de ejecución ofrece varios contenedores flexibles. Hay una serie de clases no genéricas (como TList
y TObjectList
de la unidad Contnrs
), también hay matrices dinámicas (matriz de TMyType
). Pero para obtener la mayor flexibilidad y seguridad de tipos, recomiendo usar contenedores genéricos para la mayoría de sus necesidades.
Los contenedores genéricos le brindan muchos métodos útiles para agregar, eliminar, iterar, buscar, clasificar… El compilador también sabe (y verifica) que el contenedor contiene solo elementos del tipo apropiado.
Hay tres bibliotecas que proporcionan contenedores genéricos en FPC:
- Unidad Generics.Collections y amigos (desde FPC >= 3.2.0)
- Unidad FGL
- Unidad GVector y amigos (juntos en fcl-stl)
Recomendamos utilizar la unidad Generics.Collections. Los contenedores genéricos que implementan:
- Paquete completo de caracteríscticas útiles
- Muy eficiente, en particular para acceder a diccionarios por su clave
- Compatible con FPC y Delphi.
- Nombre coherente con la biblioteca estándar.
En Castle Game Engine: utilizamos Generics.Collections de forma intensiva en todo el motor y le recomendamos que utilice Generics.Collections también en sus aplicaciones.
Las clases más importantes de la unidad Generics.Collections son:
Lista TL: Una lista genérica de tipos.
TObjectList: Una lista genérica de instancias de objetos. Puede «poseer» hijos, lo que significa que los liberará automáticamente.
TDiccionario: Un diccionario genérico.
TObjectDiccionario: Un diccionario genérico, que puede «poseer» las claves y/o valores.
Aquí le mostramos cómo usar una TObjectList genérica simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleList = specialize TObjectList<TApple>; var A: TApple; Apples: TAppleList; begin Apples := TAppleList.Create(true); try A := TApple.Create; A.Name := 'mi manzana'; Apples.Add(A); A := TApple.Create; A.Name := 'otra manzana'; Apples.Add(A); Writeln('Count: ', Apples.Count); Writeln(Apples[0].Name); Writeln(Apples[1].Name); finally FreeAndNil(Apples); end; end. |
Tenga en cuenta que algunas operaciones requieren comparar dos elementos, como ordenar y buscar (por ejemplo, mediante los métodos Sort
e IndexOf
). Los contenedores Generics.Collections utilizan para esto un comparador. El comparador predeterminado es razonable para todos los tipos, incluso para registros (en cuyo caso compara el contenido de la memoria, que es un valor predeterminado razonable al menos para buscar usando IndexOf).
Al ordenar la lista, puede proporcionar un comparador personalizado como parámetro. El comparador es una clase que implementa la interfaz IComparer. En la práctica, normalmente define la devolución de llamada adecuada y usa el método TComparer.Construct para envolver esta devolución de llamada en una instancia de IComparer. A continuación se muestra un ejemplo de cómo hacerlo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Defaults, Generics.Collections; type TApple = class Name: string; end; TAppleList = specialize TObjectList<TApple>; function CompareApples(constref Left, Right: TApple): Integer; begin Result := AnsiCompareStr(Left.Name, Right.Name); end; type TAppleComparer = specialize TComparer<TApple>; var A: TApple; L: TAppleList; begin L := TAppleList.Create(true); try A := TApple.Create; A.Name := '11'; L.Add(A); A := TApple.Create; A.Name := '33'; L.Add(A); A := TApple.Create; A.Name := '22'; L.Add(A); L.Sort(TAppleComparer.Construct(@CompareApples)); Writeln('Count: ', L.Count); Writeln(L[0].Name); Writeln(L[1].Name); Writeln(L[2].Name); finally FreeAndNil(L) end; end. |
La clase TDictionary
implementa un diccionario, también conocido como mapa (clave → valor), también conocido como matriz asociativa. Su API es un poco similar a la clase C# TDictionary. Tiene iteradores útiles para claves, valores y pares de clave→valor.
Un código de ejemplo usando un diccionario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleDictionary = specialize TDictionary<string, TApple>; var Apples: TAppleDictionary; A, FoundA: TApple; ApplePair: TAppleDictionary.TDictionaryPair; AppleKey: string; begin Apples := TAppleDictionary.Create; try A := TApple.Create; A.Name := 'mi manzana'; Apples.AddOrSetValue('manzana clave 1', A); if Apples.TryGetValue('manzana clave 1', FoundA) then Writeln('Encontrada manzana bajo la clave : "manzana clave 1", con el nombre: ' + FoundA.Name); for AppleKey in Apples.Keys do Writeln('Encontrada clave manzana: ' + AppleKey); for A in Apples.Values do Writeln('Encontrada clave manzana: ' + A.Name); for ApplePair in Apples do Writeln('Encontrada manza Clave->Valor: ' + ApplePair.Key + '->' + ApplePair.Value.Name); { La línea inferior tambien funciona, pero solo puede ser usada para modificar una clave existente en el diccionario } // Apples['manzana clave 1'] := ... ; Apples.Remove('manzana clave 1'); { Observa que TDictionary no es propietario de sus elementos por tanto debes liberarlos por ti mismo. Podemos usar TObjectDictionary para usar el método automatico} A.Free; finally FreeAndNil(Apples) end; end. |
TObjectDictionary
también puede poseer las claves y/o valores del diccionario, lo que significa que se liberarán automáticamente. Tenga cuidado de poseer solo claves y/o valores si son instancias de objetos. Si configura «property» de algún otro tipo, como un número entero (por ejemplo, si sus claves son números enteros e incluye doOwnsKeys), obtendrá un bloqueo desagradable cuando se ejecute el código.
A continuación se muestra un código de ejemplo que usa TObjectDictionary
. Compile este ejemplo con la detección de fugas de memoria, como fpc -gl -gh generics_object_dictionary.lpr
, para ver que todo se libera cuando se cierra el programa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleDictionary = specialize TObjectDictionary<string, TApple>; var Apples: TAppleDictionary; A: TApple; ApplePair: TAppleDictionary.TDictionaryPair; begin Apples := TAppleDictionary.Create([doOwnsValues]); try A := TApple.Create; A.Name := 'my apple'; Apples.AddOrSetValue('manzana clave 1', A); for ApplePair in Apples do Writeln('Encontrda manzana clave->valor: ' + ApplePair.Key + '->' + ApplePair.Value.Name); Apples.Remove('manzana clave 1'); finally FreeAndNil(Apples) end; end. |
Si prefiere usar la unidad FGL en lugar de Generics.Collections, las clases más importantes de la unidad FGL son:
Lista TFPGL: Una lista genérica de tipos.
TFPGObjectList: Una lista genérica de instancias de objetos. Puede «poseer» hijos.
TFPGMapa: Un diccionario genérico.
En la unidad FGL, TFPGList
solo se puede usar para tipos para los que se define el operador de igualdad (=). Para TFPGMap
, los operadores «mayor que» (>) y «menor que» (<) deben definirse para el tipo de clave. Si desea utilizar estas listas con tipos que no tienen operadores de comparación incorporados (por ejemplo, con registros), debe sobrecargar sus operadores como se muestra en la sobrecarga de operadores.
En Castle Game Engine incluimos una unidad CastleGenericLists
que agrega las clases TGenericStructList
y TGenericStructMap
. Son similares a TFPGList
y TFPGMap
, pero no requieren una definición de los operadores de comparación para el tipo apropiado (en su lugar, comparan los contenidos de la memoria, lo que suele ser apropiado para registros o punteros de métodos). Pero la unidad CastleGenericLists
está obsoleta desde la versión 6.3 del motor, ya que recomendamos usar Generics.Collections
en su lugar.
Si desea obtener más información sobre los genéricos, consulte Genéricos.
Clonación: TPersistent.Assign
Al copiar las instancias de clase mediante un operador de asignación simple, se copia la referencia.
1 2 3 4 5 6 7 8 9 |
var X, Y: TMyObject; begin X := TMyObject.Create; Y := X; // X y Y ahora son dos punteros a los mismos datos Y.MyField := 123; // this also changes X.MyField FreeAndNil(X); end; |
Para copiar el contenido de la instancia de clase, el enfoque estándar es derivar su clase de TPersistent
y anular su método Assign
. Una vez que se implementa correctamente en TMyObject
, lo usa así:
1 2 3 4 5 6 7 8 9 10 |
var X, Y: TMyObject; begin X := TMyObject.Create; Y := TMyObject.Create; Y.Assign(X); Y.MyField := 123; // Esto no cambia X.MyField FreeAndNil(X); FreeAndNil(Y); end; |
Para que funcione, debe implementar el método Assign
para copiar realmente los campos que desea. Debe implementar cuidadosamente el método Assign
, para copiar de una clase que puede ser descendiente de la clase actual.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Classes; type TMyClass = class(TPersistent) public MyInt: Integer; procedure Assign(Source: TPersistent); override; end; TMyClassDescendant = class(TMyClass) public MyString: string; procedure Assign(Source: TPersistent); override; end; procedure TMyClass.Assign(Source: TPersistent); var SourceMyClass: TMyClass; begin if Source is TMyClass then begin SourceMyClass := TMyClass(Source); MyInt := SourceMyClass.MyInt; // Xxx := SourceMyClass.Xxx; // Añadir nuevos campos aquí end else { Ya que TMyClass es un descendiente directo de TPersistent, este llama a la herencia SOLO cuando no puede gestinar la clase Source class. Ver los comentario más abajo } inherited Assign(Source); end; procedure TMyClassDescendant.Assign(Source: TPersistent); var SourceMyClassDescendant: TMyClassDescendant; begin if Source is TMyClassDescendant then begin SourceMyClassDescendant := TMyClassDescendant(Source); MyString := SourceMyClassDescendant.MyString; // Xxx := SourceMyClassDescendant.Xxx; // Añadir nuevos campos aquí end; { Como TMyClassDescendant tiene un antecesor, que ya sobre escribe Assign (eb TMyClass.Assign), SIEMPRE llama a la herencia, para permitir a TMyClass.Assign manejar los campos restantes. Ver los comentarios más abajo para entender el razonamiento } inherited Assign(Source); end; var C1, C2: TMyClass; CD1, CD2: TMyClassDescendant; begin // test TMyClass.Assign C1 := TMyClass.Create; C2 := TMyClass.Create; try C1.MyInt := 666; C2.Assign(C1); WriteLn('C2 state: ', C2.MyInt); finally FreeAndNil(C1); FreeAndNil(C2); end; // test TMyClassDescendant.Assign CD1 := TMyClassDescendant.Create; CD2 := TMyClassDescendant.Create; try CD1.MyInt := 44; CD1.MyString := 'blah'; CD2.Assign(CD1); WriteLn('CD2 state: ', CD2.MyInt, ' ', CD2.MyString); finally FreeAndNil(CD1); FreeAndNil(CD2); end; end. |
A veces es más cómodo anular alternativamente el método AssignTo
en la clase de origen, en lugar de anular el método Assign
en la clase de destino.
Tenga cuidado cuando llame a la herencia en la implementación de Asignación anulada. Hay dos situaciones:
Su clase es descendiente directa de la clase TPersistent. (O bien, no es un descendiente directo de TPersistent, pero ningún ancestro anuló el método Assign).
En este caso, su clase debe usar la palabra clave inherited
(para llamar a TPersistent.Assign
) solo si no puede manejar la asignación en su código.
Su clase desciende de alguna clase que ya ha anulado el método Assign.
En este caso, su clase siempre debe usar la palabra clave inherited
(para llamar al ancestro Assign
). En general, llamar a métodos heredados anulados suele ser una buena idea.
Para comprender el motivo detrás de la regla anterior (cuándo debe llamar y cuándo no debe llamar a la herencia de la implementación de Assign
) y cómo se relaciona con el método AssignTo
, veamos las implementaciones de TPersistent.Assign
y TPersistent.AssignTo
:
1 2 3 4 5 6 7 8 9 10 11 12 |
procedure TPersistent.Assign(Source: TPersistent); begin if Source <> nil then Source.AssignTo(Self) else raise EConvertError... end; procedure TPersistent.AssignTo(Destination: TPersistent); begin raise EConvertError... end; |
Nota: Esta no es la implementación exacta de TPersistent
. Copié el código de la biblioteca estándar de FPC, pero luego lo simplifiqué para ocultar detalles sin importancia sobre el mensaje de excepción.
Las conclusiones que se pueden sacar de lo anterior son:
- Si no se anulan ni
Assign
niAssignTo
, llamarlos dará como resultado una excepción. - Además, tenga en cuenta que no hay código en la implementación de
TPersistent
que copia automáticamente todos los campos (o todos los campos publicados) de las clases. Es por eso que debe hacerlo usted mismo, anulandoAsignar
en todas las clases. Puede usar RTTI (información de tipo de tiempo de ejecución) para eso, pero para casos simples, probablemente solo enumere los campos que se copiarán manualmente.
Cuando tiene una clase como TApple
, su implementación de TApple.Assign
generalmente se ocupa de copiar campos que son específicos de la clase TApple
(no del ancestro de TApple
, como TFruit). Por lo tanto, la implementación de TApple.Assign
generalmente verifica si la Fuente es TApple
al principio, antes de copiar los campos relacionados con Apple. Luego, llama a la herencia para permitir que TFruit
maneje el resto de los campos.
Suponiendo que implementó TFruit.Assign
y TApple.Assign
siguiendo el patrón estándar (como se muestra en el ejemplo anterior), el efecto es así:
- Si pasa la instancia de
TApple
aTApple.Assign
, funcionará y copiará todos los campos. - Si pasa la instancia de
TOrange
aTApple.Assign
, funcionará y solo copiará los campos comunes compartidos porTOrange
yTApple
. En otras palabras, los campos definidos enTFruit
- Si pasa la instancia de TWerewolf a T
Apple.Assign
, generará una excepción (porqueTApple.Assign
llamará aTFruit.Assign
, que llamará aTPersistent.Assign
, que generará una excepción).
Nota: Recuerde que al descender de TPersistent
, se publica el especificador de visibilidad predeterminado, para permitir la transmisión de descendientes de TPersistent
. No todos los tipos de campos y propiedades están permitidos en la sección publicada. Si obtiene errores relacionados con él y no le importa la transmisión, simplemente cambie la visibilidad a público. Consulte los especificadores de visibilidad.
Funciones varias del lenguaje
Rutinas locales (anidadas)
Dentro de una rutina más grande (función, procedimiento, método) puede definir una rutina auxiliar.
La rutina local puede acceder libremente (leer y escribir) a todos los parámetros de un padre y a todas las variables locales del padre que se declararon sobre él. Esto es muy poderoso. A menudo permite dividir rutinas largas en un par de pequeñas sin mucho esfuerzo (ya que no tiene que pasar toda la información necesaria en los parámetros). Tenga cuidado de no abusar de esta característica — si muchas funciones anidadas usan (e incluso cambian) la misma variable del padre, el código puede ser difícil de seguir.
Estos dos ejemplos son equivalentes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SumOfSquares(const N: Integer): Integer; function Square(const Value: Integer): Integer; begin Result := Value * Value; end; var I: Integer; begin Result := 0; for I := 0 to N do Result := Result + Square(I); end; |
Otra versión, donde dejamos que la rutina local Square acceda directamente a I:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SumOfSquares(const N: Integer): Integer; var I: Integer; function Square: Integer; begin Result := I * I; end; begin Result := 0; for I := 0 to N do Result := Result + Square; end; |
Las rutinas locales pueden llegar a cualquier profundidad — lo que significa que puede definir una rutina local dentro de otra rutina local. Así que puedes volverte loco (pero no te vuelvas loco demasiado, o el código se volverá ilegible :).
Callbacks (también conocidos como eventos, también conocidos como punteros a funciones, también conocidos como variables de procedimiento)
Permiten llamar a una función de forma indirecta, a través de una variable. La variable se puede asignar en tiempo de ejecución para apuntar a cualquier función con tipos de parámetros coincidentes y tipos de devolución.
La devolución de llamada puede ser:
- Normal, lo que significa que puede apuntar a cualquier rutina normal (no un método, no local).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
{$mode objfpc}{$H+}{$J-} function Add(const A, B: Integer): Integer; begin Result := A + B; end; function Multiply(const A, B: Integer): Integer; begin Result := A * B; end; type TMyFunction = function (const A, B: Integer): Integer; function ProcessTheList(const F: TMyFunction): Integer; var I: Integer; begin Result := 1; for I := 2 to 10 do Result := F(Result, I); end; var SomeFunction: TMyFunction; begin SomeFunction := @Add; WriteLn('1 + 2 + 3 ... + 10 = ', ProcessTheList(SomeFunction)); SomeFunction := @Multiply; WriteLn('1 * 2 * 3 ... * 10 = ', ProcessTheList(SomeFunction)); end. |
Un método: declarar con el objeto al final.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyMethod = procedure (const A: Integer) of object; TMyClass = class CurrentValue: Integer; procedure Add(const A: Integer); procedure Multiply(const A: Integer); procedure ProcessTheList(const M: TMyMethod); end; procedure TMyClass.Add(const A: Integer); begin CurrentValue := CurrentValue + A; end; procedure TMyClass.Multiply(const A: Integer); begin CurrentValue := CurrentValue * A; end; procedure TMyClass.ProcessTheList(const M: TMyMethod); var I: Integer; begin CurrentValue := 1; for I := 2 to 10 do M(I); end; var C: TMyClass; begin C := TMyClass.Create; try C.ProcessTheList(@C.Add); WriteLn('1 + 2 + 3 ... + 10 = ', C.CurrentValue); C.ProcessTheList(@C.Multiply); WriteLn('1 * 2 * 3 ... * 10 = ', C.CurrentValue); finally FreeAndNil(C); end; end. |
Tenga en cuenta que no puede pasar procedimientos/funciones globales como métodos. Son incompatibles. Si tiene que proporcionar una devolución de llamada de objeto, pero no desea crear una instancia de clase ficticia, puede pasar los métodos de clase como métodos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type TMyMethod = function (const A, B: Integer): Integer of object; TMyClass = class class function Add(const A, B: Integer): Integer class function Multiply(const A, B: Integer): Integer end; var M: TMyMethod; begin M := @TMyClass(nil).Add; M := @TMyClass(nil).Multiply; end; |
Desafortunadamente, debe escribir feo @TMyClass(nil).Add en lugar de solo @TMyClass.Add.
- Una (posiblemente) rutina local: declare with está anidada al final, y asegúrese de usar la directiva {$modeswitch nestedprocvars} para el código. Estos van de la mano con las rutinas locales (anidadas).
Funciones anonimas (Anonymous functions)
Delphi y las versiones nuevas de FPC (>= 3.3.1) soportan funciones anónimas (anynomous functions).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
{ Example of Map, ForEach methods and processing list with anonymous functions. } {$ifdef FPC} {$mode objfpc}{$H+}{$J-} {$modeswitch functionreferences} {$modeswitch anonymousfunctions} {$endif} {$apptype CONSOLE} uses SysUtils, Generics.Collections; type { Nota sobre la definición de TIntMapFunc y TIntMapProc a continuación, ¿cuál usar? En resumen, usa "reference to". Puedes asignarles funciones anónimas fiablemente tanto en Delphi como en FPC. Con Delphi 12.1, solo la versión con "reference to" se compilará. Con FPC 3.3.1, también se compilarán otras variantes. Puedes asignar funciones anónimas a cualquiera de ellas. Así que, si solo apuntas a FPC, puedes decidir qué versión usar basándote en lo que quieres asignarles *aparte* de funciones anónimas: - La primera versión (sin "of object", sin "reference to") permite almacenar una referencia a una función global, - La segunda (con "of object") permite almacenar una referencia a un método de un objeto, - La tercera (con "reference to") es la más universal, permite muchas cosas -- ver https://forum.lazarus.freepascal.org/index.php?topic=59468.0 . } TIntMapFunc = //function(const Index, Item: Integer): Integer; //function(const Index, Item: Integer): Integer of object; reference to function(const Index, Item: Integer): Integer; TIntMapProc = //procedure(const Index, Item: Integer); //procedure(const Index, Item: Integer) of object; reference to procedure(const Index, Item: Integer); TMyInts = class({$ifdef FPC}specialize{$endif} TList<Integer>) { Cambia cada elemento un la lista usando AFunc} procedure Map(const AFunc: TIntMapFunc); {Llama AProc por cada iten en la lista} procedure ForEach(const AProc: TIntMapProc); end; procedure TMyInts.Map(const AFunc: TIntMapFunc); var Index: Integer; begin for Index := 0 to Count - 1 do Items[Index] := AFunc(Index, Items[Index]); end; procedure TMyInts.ForEach(const AProc: TIntMapProc); var Index: Integer; begin for Index := 0 to Count - 1 do AProc(Index, Items[Index]); end; var MyList: TMyInts; I: Integer; F: TIntMapFunc; begin MyList := TMyInts.Create; try for I := 0 to 10 do MyList.Add(I); F := function(const Index, Item: Integer): Integer begin Result := Item + 1; end; // effectively this increases all numbers on the list by 3 MyList.Map(F); MyList.Map(F); MyList.Map(F); // change all items to their squares MyList.Map(function(const Index, Item: Integer): Integer begin Result := Item * Item; end); // print all items MyList.ForEach(procedure(const Index, Item: Integer) begin WriteLn('Index: ', Index, ', Item: ', Item); end); finally FreeAndNil(MyList) end; end. |
Más información:
- Documentación deDelphi : https://docwiki.embarcadero.com/RADStudio/Sydney/en/Anonymous_Methods_in_Delphi
- Foro de FPC : https://forum.lazarus.freepascal.org/index.php/topic,59468.0.html
- Registro de cambios FPC: https://wiki.freepascal.org/FPC_New_Features_Trunk#Support_for_Function_References_and_Anonymous_Functions
Para obtener FPC 3.3.1, recomendamos usar FpcUpDeluxe: https://castle-engine.io/fpcupdeluxe .
Genéricos
Una poderosa característica de cualquier lenguaje moderno. La definición de algo (típicamente, de una clase) se puede parametrizar con otro tipo. El ejemplo más típico es cuando necesitas crear un contenedor (una lista, diccionario, árbol, gráfico…): puedes definir una lista de tipo T, y luego especializarla para obtener instantáneamente una lista de enteros, una lista de cadenas , una lista de TMyRecord
, etc.
Los genéricos en Pascal funcionan de forma muy similar a los genéricos en C++. Lo que significa que se «expanden» en el momento de la especialización, un poco como las macros (pero mucho más seguras que las macros; por ejemplo, los identificadores se resuelven en el momento de la definición genérica, no en la especialización, por lo que no puede «inyectar» nada inesperado). comportamiento al especializar el genérico). En efecto, esto significa que son muy rápidos (pueden optimizarse para cada tipo en particular) y funcionan con tipos de cualquier tamaño. Puede usar un tipo primitivo (entero, flotante), así como un registro, así como una clase al especializar un genérico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type generic TMyCalculator<T> = class Value: T; procedure Add(const A: T); end; procedure TMyCalculator.Add(const A: T); begin Value := Value + A; end; type TMyFloatCalculator = specialize TMyCalculator<Single>; TMyStringCalculator = specialize TMyCalculator<string>; var FloatCalc: TMyFloatCalculator; StringCalc: TMyStringCalculator; begin FloatCalc := TMyFloatCalculator.Create; try FloatCalc.Add(3.14); FloatCalc.Add(1); WriteLn('FloatCalc: ', FloatCalc.Value:1:2); finally FreeAndNil(FloatCalc); end; StringCalc := TMyStringCalculator.Create; try StringCalc.Add('something'); StringCalc.Add(' more'); WriteLn('StringCalc: ', StringCalc.Value); finally FreeAndNil(StringCalc); end; end. |
Los genéricos no se limitan a las clases, también puede tener funciones y procedimientos genéricos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; { Note: this example requires FPC 3.1.1 (will not compile with FPC 3.0.0 or older). } generic function Min<T>(const A, B: T): T; begin if A < B then Result := A else Result := B; end; begin WriteLn('Min (1, 0): ', specialize Min<Integer>(1, 0)); WriteLn('Min (3.14, 5): ', specialize Min<Single>(3.14, 5):1:2); WriteLn('Min (''a'', ''b''): ', specialize Min<string>('a', 'b')); end. |
Consulte también los Contenedores (listas, diccionarios) que usan genéricos sobre clases estándar importantes que usan genéricos.
Sobrecarga
Se permiten métodos (y funciones y procedimientos globales) con el mismo nombre, siempre que tengan parámetros diferentes. En tiempo de compilación, el compilador detecta cuál desea usar, sabiendo los parámetros que pasa.
De forma predeterminada, la sobrecarga utiliza el enfoque FPC, lo que significa que todos los métodos en un espacio de nombres dado (una clase o una unidad) son iguales y ocultan los otros métodos en espacios de nombres con menos prioridad. Por ejemplo, si define una clase con los métodos Foo(Integer) y Foo(string), y desciende de una clase con el método Foo(Float), entonces los usuarios de su nueva clase no podrán acceder al método Foo( Float) fácilmente (todavía pueden — si encasillan la clase a su tipo de antepasado). Para superar esto, utilice la palabra clave de sobrecarga.
Preprocesado
Puede usar directivas de preprocesador simples para:
- compilación condicional (código dependiendo de la plataforma, o algunos modificadores personalizados)
- incluir un archivo en otro
- también puede utilizar macros sin parámetros.
Tenga en cuenta que no se permiten macros con parámetros. En general, debe evitar usar el preprocesador… a menos que esté realmente justificado. El procesamiento previo ocurre antes del análisis, lo que significa que puede «romper» la sintaxis normal del lenguaje Pascal. Esta es una característica poderosa, pero también algo sucia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
{$mode objfpc}{$H+}{$J-} unit PreprocessorStuff; interface {$ifdef FPC} { This is only defined when compiled by FPC, not other compilers (like Delphi). } procedure Foo; {$endif} { Define a NewLine constant. Here you can see how the normal syntax of Pascal is "broken" by preprocessor directives. When you compile on Unix (includes Linux, Android, macOS), the compiler sees this: const NewLine = #10; When you compile on Windows, the compiler sees this: const NewLine = #13#10; On other operating systems, the code will fail to compile, because a compiler sees this: const NewLine = ; It's a *good* thing that the compilation fails in this case -- if you will have to port the program to an OS that is not Unix, not Windows, you will be reminded by a compiler to choose the newline convention on that system. } const NewLine = {$ifdef UNIX} #10 {$endif} {$ifdef MSWINDOWS} #13#10 {$endif} ; {$define MY_SYMBOL} {$ifdef MY_SYMBOL} procedure Bar; {$endif} {$define CallingConventionMacro := unknown} {$ifdef UNIX} {$define CallingConventionMacro := cdecl} {$endif} {$ifdef MSWINDOWS} {$define CallingConventionMacro := stdcall} {$endif} procedure RealProcedureName; CallingConventionMacro; external 'some_external_library'; implementation {$include some_file.inc} // $I is just a shortcut for $include {$I some_other_file.inc} end. |
Los archivos de inclusión tienen comúnmente la extensión .inc y se usan para dos propósitos:
- El archivo de inclusión solo puede contener otras directivas del compilador, que «configuran» su código fuente. Por ejemplo, podría crear un archivo myconfig.inc con estos contenidos:
1 2 3 4 5 6 7 |
{$mode objfpc} {$H+} {$J-} {$modeswitch advancedrecords} {$ifndef VER3} {$error This code can only be compiled using FPC version at least 3.x.} {$endif} |
Ahora puede incluir este archivo usando {$I myconfig.inc} en todas sus fuentes.
- El otro uso común es dividir una unidad grande en muchos archivos, y al mismo tiempo mantenerla en una sola unidad en lo que respecta a las reglas del idioma. No abuse de esta técnica — su primer instinto debe ser dividir una sola unidad en múltiples unidades, no dividir una sola unidad en múltiples archivos de inclusión. Sin embargo, esta es una técnica útil.
- Permite evitar la «explosión» del número de unidades, al mismo tiempo que mantiene cortos los archivos de código fuente. Por ejemplo, puede ser mejor tener una sola unidad con «controles de interfaz de usuario de uso común» que crear una unidad para cada clase de control de interfaz de usuario, ya que este último enfoque haría que la cláusula típica de «usos» fuera larga (ya que un código de interfaz de usuario típico dependen de un par de clases de interfaz de usuario). Pero colocar todas estas clases de interfaz de usuario en un solo archivo myunit.pas lo convertiría en un archivo largo, difícil de navegar, por lo que dividirlo en varios archivos de inclusión puede tener sentido.
- Permite tener una interfaz de unidad multiplataforma con implementación dependiente de la plataforma fácilmente. Básicamente puedes hacer
1 2 |
{$ifdef UNIX} {$I my_unix_implementation.inc} {$endif} {$ifdef MSWINDOWS} {$I my_windows_implementation.inc} {$endif} |
A veces, esto es mejor que escribir un código largo con muchos {$ifdef UNIX}, {$ifdef MSWINDOWS} mezclados con código normal (declaraciones de variables, implementación de rutinas). El código es más legible de esta manera. Incluso puede usar esta técnica de manera más agresiva, usando la opción de línea de comandos -Fi de FPC para incluir algunos subdirectorios solo para plataformas específicas. Luego puede tener muchas versiones del archivo de inclusión {$I my platform_specific_implementation.inc} y simplemente incluirlas, permitiendo que el compilador encuentre la versión correcta.
Registros
Record es solo un contenedor para otras variables. Es como una clase mucho más simplificada: no hay herencia ni métodos virtuales. Es como una estructura en lenguajes tipo C
Si usa la directiva {$modeswitch advancedrecords}, los registros pueden tener métodos y especificadores de visibilidad. En general, las características del idioma que están disponibles para las clases y que no rompen el diseño de memoria predecible simple de un registro, son posibles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{$mode objfpc}{$H+}{$J-} {$modeswitch advancedrecords} type TMyRecord = record public I, Square: Integer; procedure WriteLnDescription; end; procedure TMyRecord.WriteLnDescription; begin WriteLn('Square of ', I, ' is ', Square); end; var A: array [0..9] of TMyRecord; R: TMyRecord; I: Integer; begin for I := 0 to 9 do begin A[I].I := I; A[I].Square := I * I; end; for R in A do R.WriteLnDescription; end. |
En el Object Pascal moderno, su primer instinto debería ser diseñar una clase, no un registro, porque las clases están repletas de funciones útiles, como constructores y herencia.
Pero los registros siguen siendo muy útiles cuando necesita velocidad o un diseño de memoria predecible:
- Los registros no tienen ningún constructor o destructor. Simplemente define una variable de un tipo de registro. Tiene contenidos indefinidos (basura de memoria) al principio (excepto los tipos administrados automáticamente, como cadenas; se garantiza que se inicializarán para estar vacíos y se finalizarán para liberar el recuento de referencias). Por lo tanto, debe tener más cuidado cuando se trata de registros, pero le brinda cierta ganancia de rendimiento.
- Las matrices de registros son muy lineales en la memoria, por lo que son compatibles con la memoria caché.
- El diseño de la memoria de los registros (tamaño, relleno entre campos) está claramente definido en algunas situaciones: cuando solicita el diseño C o cuando utiliza un registro empaquetado. Esto es útil:
- para comunicarse con bibliotecas escritas en otros lenguajes de programación, cuando exponen una API basada en registros
- para leer y escribir archivos binarios
- para hacer trucos sucios de bajo nivel (como encasillar inseguro un tipo a otro, ser consciente de su representación de memoria).
- Los registros también pueden tener partes de casos, que funcionan como uniones en lenguajes similares a C. Permiten tratar la misma pieza de memoria como de un tipo diferente, según sus necesidades. Como tal, esto permite una mayor eficiencia de la memoria en algunos casos. Y permite más trucos inseguros sucios y de bajo nivel 🙂
Objetos al estilo antiguo
En los viejos tiempos, Turbo Pascal introdujo otra sintaxis para la funcionalidad de clase, utilizando la palabra clave de objeto. Es algo así como una mezcla entre el concepto de un disco y una clase moderna.
- Los objetos de estilo antiguo se pueden asignar/liberar, y durante esa operación puede llamar a su constructor/destructor.
- Pero también pueden declararse y usarse simplemente, como registros. Un registro simple o un tipo de objeto no es una referencia (puntero) a algo, son simplemente los datos. Esto los hace cómodos para datos pequeños, donde la asignación de llamadas / gratis sería molesta.
- Los objetos de estilo antiguo ofrecen herencia y métodos virtuales, aunque con pequeñas diferencias con las clases modernas. Tenga cuidado — sucederán cosas malas si intenta usar un objeto sin llamar a su constructor, y el objeto tiene métodos virtuales.
Se desaconseja utilizar los objetos de estilo antiguo en la mayoría de los casos. Las clases modernas proporcionan mucha más funcionalidad. Y cuando sea necesario, los registros (incluidos los registros avanzados) se pueden utilizar para el rendimiento. Estos conceptos suelen ser una mejor idea que los objetos de estilo antiguo.
Punteros
Puede crear un puntero a cualquier otro tipo. El puntero para escribir TMyRecord se declara como ^TMyRecord y, por convención, se llama PMyRecord. Este es un ejemplo tradicional de una lista enlazada de enteros usando registros:
1 2 3 4 5 6 |
type PMyRecord = ^TMyRecord; TMyRecord = record Value: Integer; Next: PMyRecord; end; |
Tenga en cuenta que la definición es recursiva (el tipo PMyRecord se define mediante el tipo TMyRecord, mientras que TMyRecord se define mediante PMyRecord). Se permite definir un tipo puntero a un tipo aún no definido, siempre que se resuelva dentro del mismo bloque de tipo.
Puede asignar y liberar punteros usando los métodos New/Dispose, o (más bajo nivel, no tipo seguro) métodos GetMem/FreeMem. Elimina la referencia del puntero (para acceder a las cosas señaladas por) agrega el operador ^ (por ejemplo, MyInteger := MyPointerToInteger^). Para realizar la operación inversa, que consiste en obtener un puntero de una variable existente, se antepone el operador @ (por ejemplo, MyPointerToInteger := @MyInteger).
También hay un tipo de puntero sin tipo, similar a void* en lenguajes tipo C. Es completamente inseguro y puede encasillarse en cualquier otro tipo de puntero.
Recuerde que una instancia de clase también es, de hecho, un puntero, aunque no requiere ningún operador ^ o @ para usarlo. Una lista enlazada usando clases es ciertamente posible, sería simplemente esto:
1 2 3 4 5 |
type TMyClass = class Value: Integer; Next: TMyClass; end; |
Sobrecarga de operadores
Puede anular el significado de muchos operadores de idiomas, por ejemplo, para permitir la adición y multiplicación de sus tipos personalizados. Como esto:
1 2 3 4 5 6 7 8 9 10 11 12 |
{$mode objfpc}{$H+}{$J-} uses StrUtils; operator* (const S: string; const A: Integer): string; begin Result := DupeString(S, A); end; begin WriteLn('bla' * 10); end. |
También puede anular operadores en clases. Dado que generalmente crea nuevas instancias de sus clases dentro de la función del operador, la persona que llama debe recordar liberar el resultado.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyClass = class MyInt: Integer; end; operator* (const C1, C2: TMyClass): TMyClass; begin Result := TMyClass.Create; Result.MyInt := C1.MyInt * C2.MyInt; end; var C1, C2: TMyClass; begin C1 := TMyClass.Create; try C1.MyInt := 12; C2 := C1 * C1; try WriteLn('12 * 12 = ', C2.MyInt); finally FreeAndNil(C2); end; finally FreeAndNil(C1); end; end. |
También puede anular operadores en registros. Esto suele ser más fácil que sobrecargarlos para las clases, ya que la persona que llama no tiene que lidiar con la gestión de la memoria.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyRecord = record MyInt: Integer; end; operator* (const C1, C2: TMyRecord): TMyRecord; begin Result.MyInt := C1.MyInt * C2.MyInt; end; var R1, R2: TMyRecord; begin R1.MyInt := 12; R2 := R1 * R1; WriteLn('12 * 12 = ', R2.MyInt); end. |
Para registros, se recomienda usar {$modeswitch advancedrecords} y anular operadores como operadores de clase dentro del registro. Esto permite utilizar clases genéricas que dependen de la existencia de algún operador (como TFPGList, que depende de la disponibilidad del operador de igualdad) con dichos registros. De lo contrario, no se encontraría la definición «global» de un operador (no dentro del registro) (porque no está disponible en el código que implementa TFPGList), y no podría especializar una lista como special TFPGList.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
{$mode objfpc}{$H+}{$J-} {$modeswitch advancedrecords} uses SysUtils, FGL; type TMyRecord = record MyInt: Integer; class operator+ (const C1, C2: TMyRecord): TMyRecord; class operator= (const C1, C2: TMyRecord): boolean; end; class operator TMyRecord.+ (const C1, C2: TMyRecord): TMyRecord; begin Result.MyInt := C1.MyInt + C2.MyInt; end; class operator TMyRecord.= (const C1, C2: TMyRecord): boolean; begin Result := C1.MyInt = C2.MyInt; end; type TMyRecordList = specialize TFPGList<TMyRecord>; var R, ListItem: TMyRecord; L: TMyRecordList; begin L := TMyRecordList.Create; try R.MyInt := 1; L.Add(R); R.MyInt := 10; L.Add(R); R.MyInt := 100; L.Add(R); R.MyInt := 0; for ListItem in L do R := ListItem + R; WriteLn('1 + 10 + 100 = ', R.MyInt); finally FreeAndNil(L); end; end. |
Características de las clases avanzadas
Privado y estrictamente privado
El especificador de visibilidad privada (private
) significa que el campo (o método) no es accesible fuera de esta clase. Pero permite una excepción: todo el código definido en la misma unidad puede romper esto y acceder a campos y métodos privados. Un programador de C++ diría que en Pascal todas las clases dentro de una sola unidad son amigas. Esto suele ser útil y no rompe la encapsulación, ya que está limitado a una unidad.
Sin embargo, si crea unidades más grandes, con muchas clases (que no están estrechamente integradas entre sí), es más seguro usar la privacidad estricta (strict private
). Significa que el campo (o método) no es accesible fuera de este período de clase. Sin excepciones.
De manera similar, hay visibilidad protegida (protected
)(visible para los descendientes o amigos en la misma unidad) y protección estricta (strict protected
)(visible para los descendientes, punto).
Más cosas dentro de clases y clases anidadas
Puede abrir una sección de constantes (const
) o tipos (type
) dentro de una clase. De esta manera, incluso puede definir una clase dentro de una clase. Los especificadores de visibilidad funcionan como siempre, en particular, la clase anidada puede ser privada (no visible para el mundo exterior), lo que suele ser útil.
Tenga en cuenta que para declarar un campo después de una constante o tipo, deberá abrir un bloque var
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
type TMyClass = class private type TInternalClass = class Velocity: Single; procedure DoSomething; end; var FInternalClass: TInternalClass; public const DefaultVelocity = 100.0; constructor Create; destructor Destroy; override; end; constructor TMyClass.Create; begin inherited; FInternalClass := TInternalClass.Create; FInternalClass.Velocity := DefaultVelocity; FInternalClass.DoSomething; end; destructor TMyClass.Destroy; begin FreeAndNil(FInternalClass); inherited; end; { Fíjate que el la definición del métodoslleva el prefijo "TMyClass.TInternalClass" } procedure TMyClass.TInternalClass.DoSomething; begin end; |
Métodos de clase
Estos son métodos a los que puede llamar con una referencia de clase (TMyClass
), no son necesariamente una instancia de una clase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type TEnemy = class procedure Kill; class procedure KillAll; end; var E: TEnemy; begin E := TEnemy.Create; try E.Kill; finally FreeAndNil(E) end; TEnemy.KillAll; end; |
Tenga en cuenta que pueden ser virtuales — tiene sentido, y a veces es muy útil, cuando se combinan con referencias de clase.
Los métodos de una clase también pueden estar limitados por los especificadores de visibilidad, como privado (private
) o protegido (protected
). Al igual que los métodos regulares.
Tenga en cuenta que un constructor siempre actúa como un método de una clase cuando se llama de forma normal (MyInstance := TMyClass.Create(…);
). Aunque también es posible llamar a un constructor desde dentro de la propia clase, como un método normal, y luego actúa como un método normal. Esta es una característica útil para «encadenar» constructores, cuando un constructor (por ejemplo, sobrecargado puede tomar un parámetro entero) hace algún trabajo y luego llama a otro constructor (por ejemplo, sin parámetros).
Referencias de clase
La referencia de una clase le permite elegir la clase en tiempo de ejecución, por ejemplo, para llamar a un método de una clase o constructor sin conocer la clase exacta en tiempo de compilación. Es un tipo declarado como clase de TMyClass
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
type TMyClass = class(TComponent) end; TMyClass1 = class(TMyClass) end; TMyClass2 = class(TMyClass) end; TMyClassRef = class of TMyClass; var C: TMyClass; ClassRef: TMyClassRef; begin // obviamente puede hacer esto: C := TMyClass.Create(nil); FreeAndNil(C); C := TMyClass1.Create(nil); FreeAndNil(C); C := TMyClass2.Create(nil); FreeAndNil(C); // Además, usando referencias a una clase puede hacer esto: ClassRef := TMyClass; C := ClassRef.Create(nil); FreeAndNil(C); ClassRef := TMyClass1; C := ClassRef.Create(nil); FreeAndNil(C); ClassRef := TMyClass2; C := ClassRef.Create(nil); FreeAndNil(C); end; |
Las referencias de una clase se pueden combinar con métodos de clase virtual. Esto da un efecto similar al uso de clases con métodos virtuales — el método real que se ejecutará se determina en tiempo de ejecución.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
type TMyClass = class(TComponent) class procedure DoSomething; virtual; abstract; end; TMyClass1 = class(TMyClass) class procedure DoSomething; override; end; TMyClass2 = class(TMyClass) class procedure DoSomething; override; end; TMyClassRef = class of TMyClass; var C: TMyClass; ClassRef: TMyClassRef; begin ClassRef := TMyClass1; ClassRef.DoSomething; ClassRef := TMyClass2; ClassRef.DoSomething; { Y esto ejecutará una excepción en tiempo de ejecución, ya que DoSomething es abstracto en en TMyClass. } ClassRef := TMyClass; ClassRef.DoSomething; end; |
Si tiene una instancia y desea obtener una referencia a su clase (no la clase declarada, sino la clase descendiente final utilizada en su construcción), puede usar la propiedad ClassType
. El tipo declarado de ClassType
es TClass
, que significa clase de TObject
. A menudo, puede encasillarlo de manera segura en algo más específico, cuando sabe que la instancia es algo más específico que TObject
.
En particular, puede usar la referencia ClassType
para llamar a métodos virtuales, incluidos los constructores virtuales. Esto le permite crear un método como Clone
que construye una instancia de la clase de tiempo de ejecución exacta del objeto actual. Puede combinarlo con Cloning: TPersistent.Assign
para tener un método que devuelva un clon recién construido de la instancia actual.
Recuerda que solo funciona cuando el constructor de tu clase es virtual. Por ejemplo, se puede usar con los descendientes estándar de TComponent
, ya que todos deben anular el constructor virtual TComponent.Create(AOwner: TComponent)
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
type TMyClass = class(TComponent) procedure Assign(Source: TPersistent); override; function Clone(AOwner: TComponent): TMyClass; end; TMyClassRef = class of TMyClass; function TMyClass.Clone(AOwner: TComponent): TMyClass; begin // Esto siempre crea una instancia exacta de TMyClass // Result := TMyClass.Create(AOwner); // Esto, potencialmente, crea una instancia de descendiente de TMyClass Result := TMyClassRef(ClassType).Create(AOwner); Result.Assign(Self); end; |
Métodos de una clase estática
Para comprender los métodos de una clase estáticos, debe comprender cómo funcionan los métodos de clase normales (descritos en las secciones anteriores). Internamente, los métodos de clase normales reciben una referencia de una clase de su clase (se pasa a través de un primer parámetro del método oculto e implícitamente agregado). Incluso se puede acceder a esta referencia de clase explícitamente usando la palabra clave Self dentro del método de clase. Por lo general, es algo bueno: esta referencia de clase le permite llamar a métodos de clases virtuales (a través de la tabla de métodos virtuales de la clase).
Si bien esto es bueno, hace que los métodos de clase normales sean incompatibles cuando se asignan a un puntero de procedimiento global. Es decir, esto no compilará:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{$mode objfpc}{$H+}{$J-} type TMyCallback = procedure (A: Integer); TMyClass = class class procedure Foo(A: Integer); end; class procedure TMyClass.Foo(A: Integer); begin end; var Callback: TMyCallback; begin // Error: TMyClass.Foo es compatible con TMyCallBack Callback := @TMyClass(nil).Foo; end. |
Nota: En el modo Delphi, podría escribir TMyClass
.Foo en lugar de un feo TMyClass(nil).Foo
en el ejemplo anterior. Es cierto que TMyClass.Foo
se ve mucho más elegante y el compilador también lo verifica mejor. Usar TMyClass(nil).Foo
es un truco… desafortunadamente, es necesario (por ahora) en el modo ObjFpc que se presenta a lo largo de este libro.
En cualquier caso, la asignación de TMyClass.Foo
a la devolución de llamada anterior aún fallaría en el modo Delphi, exactamente por las mismas razones.
El ejemplo anterior no se compila porque Callback
es incompatible con el método de la clase Foo
. Y es incompatible porque internamente el método de la clase tiene ese parámetro implícito oculto especial para pasar una referencia de una clase.
Una forma de corregir el ejemplo anterior es cambiar la definición de TMyCallback
. Funcionará si se trata de una devolución de la llamada de método, declarada como TMyCallback = procedimiento (A: Integer) of object;
. Pero a veces, no es deseable.
Aquí viene el método de una clase estática. Es, en esencia, solo un procedimiento/función global, pero su espacio de nombres está limitado dentro de la clase. No tiene ninguna referencia de clase implícita (y por lo tanto, no puede ser virtual y no puede llamar a métodos de clase virtual). Pero el lado positivo, es compatible con las devoluciones de llamadas normales (sin objetos). Así que esto funcionará:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{$mode objfpc}{$H+}{$J-} type TMyCallback = procedure (A: Integer); TMyClass = class class procedure Foo(A: Integer); static; end; class procedure TMyClass.Foo(A: Integer); begin end; var Callback: TMyCallback; begin Callback := @TMyClass.Foo; end. |
Propiedades de clase y variables
Una propiedad de una clase es una propiedad a la que se puede acceder a través de una referencia de una clase (no necesita una instancia de clase).
Es una analogía bastante directa de una propiedad regular (ver Propiedades). Para una propiedad de una clase, define un getter y/o un setter. Pueden hacer referencia a una variable de clase o a un método de clase estático.
Una variable de una clase es, lo adivinó, como un campo normal, pero no necesita una instancia de clase para acceder a ella. En efecto, es como una variable global, pero con el espacio de nombres limitado a la clase contenedora. Se puede declarar dentro de la sección class var
de la clase. Alternativamente, se puede declarar siguiendo la definición de campo normal con la palabra clave static
.
Y un método de una clase estática es como un procedimiento/función global, pero con el espacio de nombres limitado a la clase contenedora. Para obtener más información sobre los métodos de clase estáticos en la sección anterior, consulte Métodos de clase estáticos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{$mode objfpc}{$H+}{$J-} type TMyClass = class strict private // Alternative: // FMyProperty: Integer; static; class var FMyProperty: Integer; class procedure SetMyProperty(const Value: Integer); static; public class property MyProperty: Integer read FMyProperty write SetMyProperty; end; class procedure TMyClass.SetMyProperty(const Value: Integer); begin Writeln('MyProperty cambia!'); FMyProperty := Value; end; begin TMyClass.MyProperty := 123; Writeln('TMyClass.MyProperty is now ', TMyClass.MyProperty); end. |
Ayudantes de clase (Helpers)
El método es solo un procedimiento o función dentro de una clase. Desde el exterior de la clase, la llama con una sintaxis especial MyInstance.MyMethod(…)
. Después de un tiempo te acostumbras a pensar que si quiero hacer una acción Acción en la instancia X, escribo X.Action(…)
.
Pero a veces, necesita implementar algo que conceptualmente es una acción en la clase TMyClass
sin modificar el código fuente de TMyClass
. A veces es porque no es su código fuente y no quiere cambiarlo. A veces se debe a las dependencias — agregar un método como Render
a una clase como TMy3DObject
parece una idea sencilla, pero tal vez la implementación base de la clase TMy3DObject
debería mantenerse independiente del código de renderizado. Sería mejor «mejorar» una clase existente, agregarle funcionalidad sin cambiar su código fuente.
Una forma sencilla de hacerlo es crear un procedimiento global que tome una instancia de TMy3DObject como su primer parámetro.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type TMy3DObjectHelper = class helper for TMy3DObject procedure Render(const Color: TColor); end; procedure TMy3DObjectHelper.Render(const Color: TColor); var I: Integer; begin {Fíjate que accedemos a ShapesCount, Aquí, shaoe sin ningún calificativo} for I := 0 to ShapesCount - 1 do RenderMesh(Shape[I].Mesh, Color); end; |
El concepto más general es «type helper». Al usarlos, puede agregar métodos incluso a tipos primitivos, como enteros o enumeraciones. También puede agregar «ayudantes de registro» a (lo adivinó…) registros. Consulte http://lists.freepascal.org/fpc-announce/2013-February/000587.html.
Constructores virtuales, destructores
El nombre del destructor siempre es Destroy
, es virtual (ya que puede llamarlo sin conocer la clase exacta en tiempo de compilación) y sin parámetros.
El nombre del constructor es por convención Create.
Puede cambiar este nombre, aunque tenga cuidado con esto: si define CreateMy
, siempre redefina también el nombre Create
, de lo contrario, el usuario aún puede acceder al constructor Create
del antepasado, sin pasar por su constructor CreateMy
.
En el TObject
base no es virtual, y al crear descendientes eres libre de cambiar los parámetros. El nuevo constructor ocultará el constructor en el ancestro (nota: no ponga aquí sobrecarga, a menos que quiera romperlo).
En los descendientes de TComponent
, debe anular su constructor Create (AOwner: TComponent);
. Para la funcionalidad de transmisión (streaming), para crear una clase sin conocer su tipo en el momento de la compilación, es muy útil tener constructores virtuales (consulte Referencias de una clase más arriba).
Una excepción en el constructor.
¿Qué sucede si ocurre una excepción durante un constructor? La línea:
1 |
X := TMyClass.Create; |
no se ejecuta hasta el final en este caso, no se puede asignar X, entonces, ¿quién limpiará después de una clase parcialmente construida?
La solución de Object Pascal es que, en caso de que ocurra una excepción dentro de un constructor, se llama al destructor. Esta es una razón por la que su destructor debe ser robusto, lo que significa que debería funcionar en cualquier circunstancia, incluso en una instancia de clase creada a medias. Por lo general, esto es fácil si libera todo de forma segura, como FreeAndNil.
También tenemos que depender en tales casos de que se garantice que la memoria de la clase se ponga a cero justo antes de que se ejecute el código del constructor. Entonces sabemos que al principio, todas las referencias de clase son nulas, todos los números enteros son 0 y así sucesivamente.
Entonces, a continuación, funciona sin fugas de memoria:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TGun = class end; TPlayer = class Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end; constructor TPlayer.Create; begin inherited; Gun1 := TGun.Create; raise Exception.Create('¡Elevando una excepción desde el constructor!'); Gun2 := TGun.Create; end; destructor TPlayer.Destroy; begin { en caso que el constructor rompa, podemos tener Gun1 <> nil and Gun2 = nil now. Debemos gestionarlo ...Actualmente en este caso, FreeAndNil, lo gestiona sin ningún esfuerzo adicional, porque FreeAndNil comprueba si la instancia es Nil, antes de llamar a su destructor.} FreeAndNil(Gun1); FreeAndNil(Gun2); inherited; end; begin try TPlayer.Create; except on E: Exception do WriteLn('Capturada ' + E.ClassName + ': ' + E.Message); end; end. |
Interfaces
Interfaces básicas (CORBA)
Una interfaz declara una API de la misma forma que una clase pero, a diferencia de estas, no define su
implementación. Una clase sólo puede tener una clase padre, pero puede implementar muchas interfaces.
Se puede hacer un cast de una clase a cualquiera de las interfaces que soporta y, de esta forma, llamar a
los métodos a través de esa interfaz. Esto permite tratar de forma uniforme a clases que no descienden
unas de otras pero que tienen una funcionalidad común. Son útiles cuando una herencia simple de clase no
es suficiente para alcanzar la funcionalidad deseada.
Las interfaces CORBA en Object Pascal trabajan de forma muy parecida a las interfaces de Java
- (https://docs.oracle.com/javase/tutorial/java/concepts/interface.html)
- (https://msdn.microsoft.com/en -us/library/ms173156.aspx).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
{$mode objfpc}{$H+}{$J-} {$interfaces corba} uses SysUtils, Classes; type IMyInterface = interface ['{79352612-668B-4E8C-910A-26975E103CAC}'] procedure Shoot; end; TMyClass1 = class(IMyInterface) procedure Shoot; end; TMyClass2 = class(IMyInterface) procedure Shoot; end; TMyClass3 = class procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Disparando... '); I.Shoot; end; var C1: TMyClass1; C2: TMyClass2; C3: TMyClass3; begin C1 := TMyClass1.Create; C2 := TMyClass2.Create; C3 := TMyClass3.Create; try if C1 is IMyInterface then UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); // La interface "C3", más abajo, es falsa, // así "UseThroughInterface(C3 as IMyInterface)" no se ejecutará. if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end; end. |
10.2. CORBA and COM types of interfaces
¿Porqué las interfaces del ejemplo anterior se denominan “CORBA”?
La denominación CORBA no es muy afortunada. Un nombre más adecuado sería interfaces desnudas o básicas (bare interfaces), dado que estas interfaces son una “característica básica del lenguaje”.
Utilízalas cuando desees asimilar varias clases a la misma interfaz, dado que comparten la misma API.
A pesar de que estas interfaces pueden utilizarse con la tecnología CORBA (Common Object Request Broker Architecture) (ver información sobre CORBA en Wikipedia), no están ligadas a esta tecnología en
modo alguno.
¿Se necesita la declaración {$interfaces corba}?
Sí, porque por defecto creas interfaces COM. Esto se puede indicar explícitamente diciendo {$interfaces com}, pero por lo general no es necesario ya que es el estado predeterminado.
Y no aconsejo usar interfaces COM, especialmente si está buscando algo equivalente a las interfaces de otros lenguajes de programación. Las interfaces CORBA en Pascal son exactamente lo que espera si busca algo equivalente a las interfaces en C# y Java. Mientras que las interfaces COM traen características adicionales que posiblemente no desee.
Tenga en cuenta que la declaración {$interfaces xxx}
solo afecta a las interfaces que no tienen ningún ancestro explícito (solo la palabra clave interface, no interface(ISomeAncestor)
). Cuando una interfaz tiene un ancestro, tiene el mismo tipo que el ancestro, independientemente de la declaración {$interfaces xxx}
.
¿Qué son las interfaces COM?
La interfaz COM es sinónimo de una interfaz que desciende de una interfaz especial IUnknown
. Descendente de IUnknown
:
Requiere que sus clases definan los métodos _AddRef
y _ReleaseRef
. La implementación adecuada de estos métodos puede administrar la vida útil de sus objetos utilizando el recuento de referencias.
Agrega el método QueryInterface
.
Permite interactuar con la tecnología COM (Component Object Model).
¿Por qué aconseja no utilizar las interfaces COM?
Debido a que las interfaces COM «enredan» dos características que deberían no estar relacionadas (ortogonales) en mi opinión: herencia múltiple y conteo de referencias. Otros lenguajes de programación usan correctamente conceptos separados para estas dos características. Para ser claros: el conteo de referencias, que proporciona una gestión automática de la memoria (en situaciones simples, es decir, sin ciclos), es un concepto muy útil. Pero enredar esta característica con interfaces (en lugar de convertirlas en características ortogonales) no es limpio a mis ojos. Definitivamente no coincide con mis casos de uso.
- veces quiero enviar mis clases (que de otro modo no estarían relacionadas) a una interfaz común.
- A veces quiero administrar la memoria usando el enfoque de conteo de referencias.
- Quizás algún día quiera interactuar con la tecnología COM.
Pero todas estas son necesidades separadas y no relacionadas. Enredarlos en una característica de un solo idioma es contraproducente en mi experiencia. Causa problemas reales:
- Si quiero la característica de enviar clases a una API de interfaz común, pero no quiero el mecanismo de conteo de referencias (quiero liberar objetos manualmente), entonces las interfaces COM son problemáticas. Incluso cuando el conteo de referencias está deshabilitado por una implementación especial de _AddRef y _ReleaseRef, aún debe tener cuidado de nunca tener una referencia de interfaz temporal colgada, después de haber liberado la instancia de clase. Más detalles al respecto en la siguiente sección.
- Si quiero la función de conteo de referencias, pero no necesito una jerarquía de interfaz para representar algo diferente a la jerarquía de clases, entonces tengo que duplicar la API de mis clases en las interfaces. Creando así una sola interfaz para cada clase. Esto es contraproducente. Preferiría tener punteros inteligentes como una característica de idioma separada, no enredada con las interfaces (y afortunadamente, está llegando :).
Es por eso que aconsejo usar interfaces de estilo CORBA y la directiva {$interfaces corba}, en todo el código moderno que trata con interfaces.
Solo si necesita «recuento de referencias» y «herencia múltiple» al mismo tiempo, utilice las interfaces COM. Además, Delphi solo tiene interfaces COM por ahora, por lo que debe usar interfaces COM si su código debe ser compatible con Delphi.
¿Podemos tener un conteo de referencias con interfaces CORBA?
Sí. Simplemente agregue los métodos _AddRef / _ReleaseRef. No es necesario descender de la interfaz IUnknown. Aunque en la mayoría de los casos, si desea contar referencias con sus interfaces, también puede usar interfaces COM.
GUID de interfaces
Los GUID son los caracteres aparentemente aleatorios [‘{ABCD1234-…}’] que ve colocados en cada definición de interfaz. Sí, son solo al azar. Desafortunadamente, son necesarios.
Los GUID no tienen sentido si no planea integrarse con tecnologías de comunicación como COM o CORBA. Pero son necesarios, por razones de implementación. No se deje engañar por el compilador, que desafortunadamente le permite declarar interfaces sin GUID.
Sin los GUID (únicos), el operador is tratará sus interfaces de la misma manera. En efecto, devolverá verdadero si su clase admite cualquiera de sus interfaces. La función mágica Supports(ObjectInstance, IMyInterface) se comporta un poco mejor aquí, ya que se niega a compilarse para interfaces sin un GUID. Esto es cierto para las interfaces CORBA y COM, a partir de FPC 3.0.0.
Por lo tanto, para estar seguro, siempre debe declarar un GUID para su interfaz. Puede usar el generador GUID de Lazarus (Ctrl + Shift + G atajo en el editor). O puede utilizar un servicio en línea como https://www.guidgenerator.com/.
O puede escribir su propia herramienta para esto, usando las funciones CreateGUID
y GUIDToString
en RTL. Vea el ejemplo a continuación:
1 2 3 4 5 6 7 8 9 10 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; var MyGuid: TGUID; begin Randomize; CreateGUID(MyGuid); WriteLn('[''' + GUIDToString(MyGuid) + ''']'); end. |
Interfaces de conteo de referencia (COM)
Las interfaces COM traen dos características adicionales:
- integración con COM (una tecnología de Windows, también disponible en Unix a través de XPCOM, utilizada por Mozilla).
- conteo de referencias (lo que le brinda destrucción automática cuando todas las referencias de la interfaz quedan fuera del alcance).
Cuando utilice interfaces COM, debe conocer su mecanismo de destrucción automática y su relación con la tecnología COM.
En la práctica, esto significa que:
- Su clase necesita implementar métodos mágicos _AddRef, _Release y QueryInterface. O descender de algo que ya los implementa. Una implementación particular de estos métodos en realidad puede habilitar o deshabilitar la función de conteo de referencias de las interfaces COM (aunque deshabilitarla es algo peligroso — vea el siguiente punto).
- La clase estándar TInterfacedObject implementa estos métodos para habilitar el conteo de referencias.
- La clase estándar TComponent implementa estos métodos para deshabilitar el conteo de referencias.
- Debe tener cuidado de liberar la clase, cuando algunas variables de interfaz pueden hacer referencia a ella. Debido a que la interfaz se libera utilizando un método virtual (porque puede contarse por referencia, incluso si piratea el método _AddRef para que no se cuente por referencia…), no puede liberar la instancia del objeto subyacente siempre que alguna variable de interfaz pueda apuntar lo. Consulte «7.7 Recuento de referencias» en el manual de FPC (http://freepascal.org/docs-html/ref/refse47.html).
El enfoque más seguro para usar las interfaces COM es:
- aceptar el hecho de que son contados por referencia,
- derivar las clases apropiadas de TInterfacedObject,
- y evite usar la instancia de clase, en lugar de acceder a la instancia siempre a través de la interfaz, permitiendo que el conteo de referencias administre la desasignación.
Este es un ejemplo de uso de interfaz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
{$mode objfpc}{$H+}{$J-} {$interfaces com} uses SysUtils, Classes; type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end; TMyClass1 = class(TInterfacedObject, IMyInterface) procedure Shoot; end; TMyClass2 = class(TInterfacedObject, IMyInterface) procedure Shoot; end; TMyClass3 = class(TInterfacedObject) procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Disparando... '); I.Shoot; end; var C1: IMyInterface; // COM se preocupa de su destrucción. C2: IMyInterface; // COM se preocupa de su destrucción C3: TMyClass3; // Debes preocuparte de su destrucción begin C1 := TMyClass1.Create as IMyInterface; C2 := TMyClass2.Create as IMyInterface; C3 := TMyClass3.Create; try UseThroughInterface(C1); // No es necesario usar el operador 'as' UseThroughInterface(C2); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); // Esto no se ejecutará finally { Las variables C1 C2 fuera del ámbito serán destruidos en este momento al contrario, C3 es una instancia de una clase, no gestionada por una interface, y tiene que ser destruida manualmente.} FreeAndNil(C3); end; end. |
Uso de interfaces COM con el recuento de referencias deshabilitado
Como se mencionó en la sección anterior, su clase puede descender de TComponent (o una clase similar como TNonRefCountedInterfacedObject y TNonRefCountedInterfacedPersistent) que deshabilita el conteo de referencias para las interfaces COM. Esto le permite usar interfaces COM y aún liberar la instancia de clase manualmente.
En este caso, debe tener cuidado de no liberar la instancia de clase cuando alguna variable de interfaz pueda hacer referencia a ella. Recuerde que cada Cx encasillado como IMyInterface también crea una variable de interfaz temporal, que puede estar presente incluso hasta el final del procedimiento actual. Por esta razón, el siguiente ejemplo usa un procedimiento UseInterfaces y libera las instancias de clase fuera de este procedimiento (cuando podemos estar seguros de que las variables de interfaz temporales están fuera del alcance).
Para evitar este lío, por lo general es mejor usar interfaces CORBA, si no desea contar referencias con sus interfaces.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
{$mode objfpc}{$H+}{$J-} {$interfaces com} uses SysUtils, Classes; type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end; TMyClass1 = class(TComponent, IMyInterface) procedure Shoot; end; TMyClass2 = class(TComponent, IMyInterface) procedure Shoot; end; TMyClass3 = class(TComponent) procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Shooting... '); I.Shoot; end; var C1: TMyClass1; C2: TMyClass2; C3: TMyClass3; procedure UseInterfaces; begin if C1 is IMyInterface then //if Supports(C1, IMyInterface) then // equivalent to "is" check above UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); end; begin C1 := TMyClass1.Create(nil); C2 := TMyClass2.Create(nil); C3 := TMyClass3.Create(nil); try UseInterfaces; finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end; end. |
Typecasting interfaces
Esta sección se aplica a las interfaces CORBA y COM (sin embargo, tiene algunas excepciones explícitas para CORBA).
- La conversión a un tipo de interfaz mediante el operador as realiza una comprobación en tiempo de ejecución. Considere este código:
1 |
UseThroughInterface(Cx as IMyInterface); |
Funciona para todas las instancias C1, C2, C3 en los ejemplos de las secciones anteriores. Si se ejecuta, cometería un error en tiempo de ejecución en el caso de C3, que no implementa IMyInterface.
El uso como operador funciona de forma coherente, independientemente de si Cx se declara como una instancia de clase (como TMyClass2) o una interfaz (como IMyInterface2).
Sin embargo, no está permitido para las interfaces CORBA.
- En su lugar, puede convertir la instancia como una interfaz implícitamente:
1 |
UseThroughInterface(Cx); |
En este caso, el typecast debe ser válido en tiempo de compilación. Entonces esto se compilará para C1 y C2 (que se declaran como clases que implementan IMyInterface). Pero no compilará para C3.
En esencia, este typecast se ve y funciona igual que para las clases normales. Siempre que se requiera una instancia de una clase TMyClass, siempre puede usar allí una variable que se declara con una clase de TMyClass o descendiente de TMyClass. La misma regla se aplica a las interfaces. No hay necesidad de ningún typecast explícito en tales situaciones.
- También puede hacer typecast utilizando IMyInterface(Cx). Como esto:
1 |
UseThroughInterface(IMyInterface(Cx)); |
Por lo general, dicha sintaxis de typecast indica un typecast y no verificado. Sucederán cosas malas si envías contenido a una interfaz incorrecta. Y eso es cierto, si conviertes una clase a una clase, o una interfaz a una interfaz, usando esta sintaxis.
Aquí hay una pequeña excepción: si Cx se declara como una clase (como TMyClass2), entonces esta es una conversión tipo que debe ser válida en tiempo de compilación. Por lo tanto, transmitir una clase a una interfaz de esta manera es un typecast y rápido (verificado en tiempo de compilación).
Para probarlo todo, juegue con este código de ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
{$mode objfpc}{$H+}{$J-} // {$interfaces corba} // note that "as" typecasts for CORBA will not compile uses Classes; type IMyInterface = interface ['{7FC754BC-9CA7-4399-B947-D37DD30BA90A}'] procedure One; end; IMyInterface2 = interface(IMyInterface) ['{A72B7008-3F90-45C1-8F4C-E77C4302AA3E}'] procedure Two; end; IMyInterface3 = interface(IMyInterface2) ['{924BFB98-B049-4945-AF17-1DB08DB1C0C5}'] procedure Three; end; TMyClass = class(TComponent, IMyInterface) procedure One; end; TMyClass2 = class(TMyClass, IMyInterface, IMyInterface2) procedure One; procedure Two; end; procedure TMyClass.One; begin Writeln('TMyClass.One'); end; procedure TMyClass2.One; begin Writeln('TMyClass2.One'); end; procedure TMyClass2.Two; begin Writeln('TMyClass2.Two'); end; procedure UseInterface2(const I: IMyInterface2); begin I.One; I.Two; end; procedure UseInterface3(const I: IMyInterface3); begin I.One; I.Two; I.Three; end; var My: IMyInterface; MyClass: TMyClass; begin My := TMyClass2.Create(nil); MyClass := TMyClass2.Create(nil); // This doesn't compile, since at compile-time it's unknown if My is IMyInterface2. // UseInterface2(My); // UseInterface2(MyClass); // This compiles and works OK. UseInterface2(IMyInterface2(My)); // This does not compile. Casting InterfaceType(ClassType) is checked at compile-time. // UseInterface2(IMyInterface2(MyClass)); // This compiles and works OK. UseInterface2(My as IMyInterface2); // This compiles and works OK. UseInterface2(MyClass as IMyInterface2); // This compiles, but will fail at runtime, with ugly "Access violation". // UseInterface3(IMyInterface3(My)); // This does not compile. Casting InterfaceType(ClassType) is checked at compile-time. // UseInterface3(IMyInterface3(MyClass)); // This compiles, but will fail at runtime, with nice "EInvalidCast: Invalid type cast". // UseInterface3(My as IMyInterface3); // This compiles, but will fail at runtime, with nice "EInvalidCast: Invalid type cast". // UseInterface3(MyClass as IMyInterface3); Writeln('Finished'); end. |
Sobre este documento
Copyright Michalis Kamburelis.
El código fuente de este documento está en AsciiDoc en https://github.com/michaliskambi/modern-pascal-introduction. Las sugerencias de correcciones y adiciones, parches y solicitudes de extracción siempre son bienvenidas 🙂 Puede comunicarse conmigo a través de GitHub o enviar un correo electrónico a michalis@castle-engine.io. Mi página de inicio es https://michalis.xyz/. Este documento está vinculado en la sección de Documentación del sitio web de Castle Game Engine https://castle-engine.io/.
Puede redistribuir e incluso modificar este documento libremente, bajo las mismas licencias que Wikipedia https://en.wikipedia.org/wiki/Wikipedia:Copyrights:
¡Gracias por leerlo!