Continuamos con la introducción al lenguaje Pascal Moderno.
Esta entrada es una traducción al español, del texto original escrito por Michalis Kamburelis.
Biblioteca en tiempo de ejecución (Run-time)
Los programas modernos deberían usar la clase TStream y sus muchos descendientes para hacer entrada/salida. Tiene muchos descendientes útiles, como TFileStream, TMemoryStream, TStringStream.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Classes; var S: TStream; InputInt, OutputInt: Integer; begin InputInt := 666; S := TFileStream.Create('my_binary_file.data', fmCreate); try S.WriteBuffer(InputInt, SizeOf(InputInt)); finally FreeAndNil(S); end; S := TFileStream.Create('my_binary_file.data', fmOpenRead); try S.ReadBuffer(OutputInt, SizeOf(OutputInt)); finally FreeAndNil(S); end; WriteLn('Read from file got integer: ', OutputInt); end. |
En Castle Game Engine: debe usar la función de descarga para crear una transmisión que obtenga datos de cualquier URL. Los archivos regulares, los recursos HTTP y HTTPS, los activos de Android y más son compatibles de esta manera. Además, para abrir el recurso dentro de los datos de tu juego (en el subdirectorio de datos) usa la URL especial castle-data:/xxx. Ejemplos:
1 2 |
EnableNetwork := true; S := Download('https://castle-engine.io/latest.zip'); |
1 |
S := Download('file:///home/michalis/my_binary_file.data'); |
1 |
S := Download('castle-data:/gui/my_image.png'); |
Para leer archivos de texto, recomendamos utilizar la clase TTextReader. Proporciona una API orientada a la línea y envuelve un TStream en su interior. El constructor TTextReader puede tomar una URL lista, o puede pasar allí su fuente TStream personalizada.
1 2 3 4 5 6 7 |
Text := TTextReader.Create('castle-data:/my_data.txt'); try while not Text.Eof do WriteLnLog('NextLine', Text.ReadLn); finally FreeAndNil(Text); end; |
Contenedores (Listas, diccionarios) usando genéricos
La biblioteca de idiomas y tiempo de ejecución ofrece varios contenedores flexibles. Hay una serie de clases no genéricas (como TList y TObjectList de la unidad Contnrs), también hay matrices dinámicas (matriz de TMyType). Pero para obtener la mayor flexibilidad y seguridad de tipos, recomiendo usar contenedores genéricos para la mayoría de sus necesidades.
Los contenedores genéricos le brindan muchos métodos útiles para agregar, eliminar, iterar, buscar, clasificar… El compilador también sabe (y verifica) que el contenedor contiene solo elementos del tipo apropiado.Hay tres bibliotecas que proporcionan contenedores genéricos en FPC ahora:
Hay tres bibliotecas que proporcionan contenedores genéricos en FPC ahora:
- Unidad Generics.Collections y amigos (desde FPC >= 3.2.0)
- unidad FGL
- Unidad GVector y amigos (juntos en fcl-stl)
Recomendamos utilizar la unidad Generics.Collections. Los contenedores genéricos que implementan:
- Paquete completo de caracteríscticas útiles
- Muy eficiente, en particular para acceder a diccionarios por su clave
- Compatible con FPC y Delphi.
- Nombre coherente con la biblioteca estándar.
En Castle Game Engine: utilizamos Generics.Collections de forma intensiva en todo el motor y le recomendamos que utilice Generics.Collections también en sus aplicaciones.
Las clases más importantes de la unidad Generics.Collections son:
Lista TL: Una lista genérica de tipos.
TObjectList: Una lista genérica de instancias de objetos. Puede «poseer» hijos, lo que significa que los liberará automáticamente.
TDiccionario: Un diccionario genérico.
TObjectDiccionario: Un diccionario genérico, que puede «poseer» las claves y/o valores.
Aquí le mostramos cómo usar una TObjectList genérica simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleList = specialize TObjectList<TApple>; var A: TApple; Apples: TAppleList; begin Apples := TAppleList.Create(true); try A := TApple.Create; A.Name := 'my apple'; Apples.Add(A); A := TApple.Create; A.Name := 'another apple'; Apples.Add(A); Writeln('Count: ', Apples.Count); Writeln(Apples[0].Name); Writeln(Apples[1].Name); finally FreeAndNil(Apples) end; end. |
Tenga en cuenta que algunas operaciones requieren comparar dos elementos, como ordenar y buscar (por ejemplo, mediante los métodos Sort e IndexOf). Los contenedores Generics.Collections utilizan para esto un comparador. El comparador predeterminado es razonable para todos los tipos, incluso para registros (en cuyo caso compara el contenido de la memoria, que es un valor predeterminado razonable al menos para buscar usando IndexOf).
Al ordenar la lista, puede proporcionar un comparador personalizado como parámetro. El comparador es una clase que implementa la interfaz IComparer. En la práctica, normalmente define la devolución de llamada adecuada y usa el método TComparer.Construct para envolver esta devolución de llamada en una instancia de IComparer. A continuación se muestra un ejemplo de cómo hacerlo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Defaults, Generics.Collections; type TApple = class Name: string; end; TAppleList = specialize TObjectList<TApple>; function CompareApples(constref Left, Right: TApple): Integer; begin Result := AnsiCompareStr(Left.Name, Right.Name); end; type TAppleComparer = specialize TComparer<TApple>; var A: TApple; L: TAppleList; begin L := TAppleList.Create(true); try A := TApple.Create; A.Name := '11'; L.Add(A); A := TApple.Create; A.Name := '33'; L.Add(A); A := TApple.Create; A.Name := '22'; L.Add(A); L.Sort(TAppleComparer.Construct(@CompareApples)); Writeln('Count: ', L.Count); Writeln(L[0].Name); Writeln(L[1].Name); Writeln(L[2].Name); finally FreeAndNil(L) end; end. |
La clase TDictionary implementa un diccionario, también conocido como mapa (clave → valor), también conocido como matriz asociativa. Su API es un poco similar a la clase C# TDictionary. Tiene iteradores útiles para claves, valores y pares de clave→valor.
Un código de ejemplo usando un diccionario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleDictionary = specialize TDictionary<string, TApple>; var Apples: TAppleDictionary; A, FoundA: TApple; ApplePair: TAppleDictionary.TDictionaryPair; AppleKey: string; begin Apples := TAppleDictionary.Create; try A := TApple.Create; A.Name := 'my apple'; Apples.AddOrSetValue('apple key 1', A); if Apples.TryGetValue('apple key 1', FoundA) then Writeln('Found apple under key "apple key 1" with name: ' + FoundA.Name); for AppleKey in Apples.Keys do Writeln('Found apple key: ' + AppleKey); for A in Apples.Values do Writeln('Found apple value: ' + A.Name); for ApplePair in Apples do Writeln('Found apple key->value: ' + ApplePair.Key + '->' + ApplePair.Value.Name); { Line below works too, but it can only be used to set an *existing* dictionary key. Instead of this, usually use AddOrSetValue to set or add a new key, as necessary. } // Apples['apple key 1'] := ... ; Apples.Remove('apple key 1'); { Note that the TDictionary doesn't own the items, you need to free them yourself. We could use TObjectDictionary to have automatic ownership mechanism. } A.Free; finally FreeAndNil(Apples) end; end. |
TObjectDictionary también puede poseer las claves y/o valores del diccionario, lo que significa que se liberarán automáticamente. Tenga cuidado de poseer solo claves y/o valores si son instancias de objetos. Si configura «propiedad» de algún otro tipo, como un número entero (por ejemplo, si sus claves son números enteros e incluye doOwnsKeys), obtendrá un bloqueo desagradable cuando se ejecute el código.
A continuación se muestra un código de ejemplo que usa TObjectDictionary. Compile este ejemplo con la detección de fugas de memoria, como fpc -gl -gh generics_object_dictionary.lpr, para ver que todo se libera cuando se cierra el programa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{$mode objfpc}{$H+}{$J-} uses SysUtils, Generics.Collections; type TApple = class Name: string; end; TAppleDictionary = specialize TObjectDictionary<string, TApple>; var Apples: TAppleDictionary; A: TApple; ApplePair: TAppleDictionary.TDictionaryPair; begin Apples := TAppleDictionary.Create([doOwnsValues]); try A := TApple.Create; A.Name := 'my apple'; Apples.AddOrSetValue('apple key 1', A); for ApplePair in Apples do Writeln('Found apple key->value: ' + ApplePair.Key + '->' + ApplePair.Value.Name); Apples.Remove('apple key 1'); finally FreeAndNil(Apples) end; end. |
Si prefiere usar la unidad FGL en lugar de Generics.Collections, las clases más importantes de la unidad FGL son:
Lista TFPGL: Una lista genérica de tipos.
TFPGObjectList: Una lista genérica de instancias de objetos. Puede «poseer» hijos.
TFPGMapa: Un diccionario genérico.
En la unidad FGL, TFPGList solo se puede usar para tipos para los que se define el operador de igualdad (=). Para TFPGMap, los operadores «mayor que» (>) y «menor que» (<) deben definirse para el tipo de clave. Si desea utilizar estas listas con tipos que no tienen operadores de comparación incorporados (por ejemplo, con registros), debe sobrecargar sus operadores como se muestra en la sobrecarga de operadores.
En Castle Game Engine incluimos una unidad CastleGenericLists que agrega las clases TGenericStructList y TGenericStructMap. Son similares a TFPGList y TFPGMap, pero no requieren una definición de los operadores de comparación para el tipo apropiado (en su lugar, comparan los contenidos de la memoria, lo que suele ser apropiado para registros o punteros de métodos). Pero la unidad CastleGenericLists está obsoleta desde la versión 6.3 del motor, ya que recomendamos usar Generics.Collections en su lugar.
Si desea obtener más información sobre los genéricos, consulte Genéricos.
Clonación: TPersistent.Assign
Al copiar las instancias de clase mediante un operador de asignación simple, se copia la referencia.
Para copiar el contenido de la instancia de clase, el enfoque estándar es derivar su clase de TPersistent y anular su método Assign. Una vez que se implementa correctamente en TMyObject, lo usa así:
1 2 3 4 5 6 7 8 9 10 |
var X, Y: TMyObject; begin X := TMyObject.Create; Y := TMyObject.Create; Y.Assign(X); Y.MyField := 123; // this does not change X.MyField FreeAndNil(X); FreeAndNil(Y); end; |
Para que funcione, debe implementar el método Asignar 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 heredado 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 heredada (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 heredada (para llamar al ancestro Asignar). En general, llamar a métodos heredados en 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 heredada de la implementación de Assign) y cómo se relaciona con el método AssignTo, veamos las implementaciones de TPersistent.Assign y TPersistent.AssignTo:
1 2 3 4 5 6 7 8 9 10 11 12 |
procedure TPersistent.Assign(Source: TPersistent); begin if Source <> nil then Source.AssignTo(Self) else raise EConvertError... end; procedure TPersistent.AssignTo(Destination: TPersistent); begin raise EConvertError... end; |
Nota: Esta no es la implementación exacta de TPersistent. Copié el código de la biblioteca estándar de FPC, pero luego lo simplifiqué para ocultar detalles sin importancia sobre el mensaje de excepción.
Las conclusiones que se pueden sacar de lo anterior son:
- Si no se anulan ni Assign 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 TAple.Assign generalmente verifica si la Fuente es TAple al principio, antes de copiar los campos relacionados con Apple. Luego, llama heredado 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.