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

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.