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

¿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

  • 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 ejecute fpc 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

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.

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.

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.

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:

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.

El else está emparejado con el último if. Entonces esto funciona como esperas:

Si bien el ejemplo con el IF anidado anterior es correcto, a menudo es mejor colocar el IF anidado dentro de un bloque BeginEnd 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.

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:

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:

Se utiliza la evaluación de «cortocircuito». Considere esta expresión:

  • Está garantizado que MyFuncion(X) se evaluará primero.
  • Y si MyFunction(x) devuelve falso, entonces la valor de la expresión será falso, y MyOtherFunction(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:

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.

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:)

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:

También se pueden usar para crear conjuntos (un campo de bits internamente):

Bucles (for, while, repeat, for .. in)

Sobre los bucles repeat y while:

Hay dos diferencias entre estos tipos de bucle:

  1. La condición de bucle tiene un significado opuesto. En mientras.. le dices cuando continuar, pero en repetir.. hasta que le dices cuando parar.
  2. 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:
  • Puede iterar sobre todos los elementos incluidos en el conjunto:
  • Y funciona en tipos de listas personalizadas, genéricas o no, como TObjectList o TFPGObjectList.

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.

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

no funciona como algunos de ustedes pensarían. Esto funcionará:

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 unidad SysUtils.
  • 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 (como StrToIntDef).
  • 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).

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:

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.

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).

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.

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:

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í:

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.

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.

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)

Herencia, is, as

Tenemos herencia y métodos virtuales.

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.

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:

Propiedades

Las propiedades son un muy buen «azúcar de sintaxis» para:

  1. Crear algo que parezca un campo (que se pueda leer y configurar) pero que debajo se realice llamando a métodos getter y setter. El uso típico es realizar algún efecto secundario (por ejemplo, volver a dibujar la pantalla) cada vez que cambia algún valor.
  2. Crear algo que parezca un campo, pero que sea de solo lectura. En efecto, es como una función constante o sin parámetros

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:

  1. Si se puede leer y cómo (leyendo directamente un campo o usando un método «captador»).
  2. 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ón setter siempre debe establecer el valor solicitado, de modo que llamar al getter 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 de MyClass.MyProperty := 123; el programador puede esperar que MyClass.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.

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.

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í:

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.

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.

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:

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).

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

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

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í:

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:

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:

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:

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

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 de TWheel, 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 variable Obj2 explícitamente en nil. 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 en nil.
  • 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 a FreeNotification , RemoveFreeNotification y anular Notificación.
  • Esto requiere que ambas clases desciendan de TComponent. Usarlo en general se reduce a llamar a FreeNotification , 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 🙂

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:

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:

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í:

Atrapando

Puede detectar una excepción como esta:

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:

O también podemos comprobar clases de excepciones

También puede reaccionar a cualquier excepción generada, si no usa ninguna expresión:

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:

Aunque normalmente es suficiente para capturar Exception:

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:

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í:

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:

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:

Probablemente sea más legible de la siguiente forma:

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.

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:

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.

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:

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:

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:

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.

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.

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í:

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.

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:

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 ni AssignTo, 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, anulando Asignar 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 a TApple.Assign, funcionará y copiará todos los campos.
  • Si pasa la instancia de TOrange a TApple.Assign, funcionará y solo copiará los campos comunes compartidos por TOrange y TApple. En otras palabras, los campos definidos en TFruit
  • Si pasa la instancia de TWerewolf a TApple.Assign, generará una excepción (porque TApple.Assign llamará a TFruit.Assign, que llamará a TPersistent.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:

Otra versión, donde dejamos que la rutina local Square acceda directamente a I:

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).

Un método: declarar con el objeto al final.

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.

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).

Más información:

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.

Los genéricos no se limitan a las clases, también puede tener funciones y procedimientos genéricos:

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.

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:

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.
    1. 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.
    2. Permite tener una interfaz de unidad multiplataforma con implementación dependiente de la plataforma fácilmente. Básicamente puedes hacer

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.

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:

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:

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:

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.

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.

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.

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.

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.

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.

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.

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).

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á:

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á:

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.

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.

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:

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:

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

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:

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:

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.

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:

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:

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:

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:

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:

Licencia Creative Commons Reconocimiento-CompartirIgual 3.0 Unported (CC BY-SA) o la Licencia de Documentación Libre GNU (GFDL) (sin versiones, sin secciones invariables, textos de portada o textos de contraportada) .

¡Gracias por leerlo!