Contenido
- 1 Funciones varias del lenguaje
- 2 Rutinas locales (anidadas)
- 3 Callbacks (también conocidos como eventos, también conocidos como punteros a funciones, también conocidos como variables de procedimiento)
- 4 Genéricos
- 5 Sobrecarga
- 6 Preprocesado
- 7 Registros
- 8 Objetos al estilo antiguo
- 9 Punteros
- 10 Sobrecarga de operadores
Funciones varias del lenguaje
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.
Rutinas locales (anidadas)
Dentro de una rutina más grande (función, procedimiento, método) puede definir una rutina auxiliar.
La rutina local puede acceder libremente (leer y escribir) a todos los parámetros de un padre y a todas las variables locales del padre que se declararon sobre él. Esto es muy poderoso. A menudo permite dividir rutinas largas en un par de pequeñas sin mucho esfuerzo (ya que no tiene que pasar toda la información necesaria en los parámetros). Tenga cuidado de no abusar de esta característica — si muchas funciones anidadas usan (e incluso cambian) la misma variable del padre, el código puede ser difícil de seguir.
Estos dos ejemplos son equivalentes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SumOfSquares(const N: Integer): Integer; function Square(const Value: Integer): Integer; begin Result := Value * Value; end; var I: Integer; begin Result := 0; for I := 0 to N do Result := Result + Square(I); end; |
Otra versión, donde dejamos que la rutina local Square acceda directamente a I:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SumOfSquares(const N: Integer): Integer; var I: Integer; function Square: Integer; begin Result := I * I; end; begin Result := 0; for I := 0 to N do Result := Result + Square; end; |
Las rutinas locales pueden llegar a cualquier profundidad — lo que significa que puede definir una rutina local dentro de otra rutina local. Así que puedes volverte loco (pero no te vuelvas loco demasiado, o el código se volverá ilegible :).
Callbacks (también conocidos como eventos, también conocidos como punteros a funciones, también conocidos como variables de procedimiento)
Permiten llamar a una función de forma indirecta, a través de una variable. La variable se puede asignar en tiempo de ejecución para apuntar a cualquier función con tipos de parámetros coincidentes y tipos de devolución.
La devolución de llamada puede ser:
- Normal, lo que significa que puede apuntar a cualquier rutina normal (no un método, no local).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
{$mode objfpc}{$H+}{$J-} function Add(const A, B: Integer): Integer; begin Result := A + B; end; function Multiply(const A, B: Integer): Integer; begin Result := A * B; end; type TMyFunction = function (const A, B: Integer): Integer; function ProcessTheList(const F: TMyFunction): Integer; var I: Integer; begin Result := 1; for I := 2 to 10 do Result := F(Result, I); end; var SomeFunction: TMyFunction; begin SomeFunction := @Add; WriteLn('1 + 2 + 3 ... + 10 = ', ProcessTheList(SomeFunction)); SomeFunction := @Multiply; WriteLn('1 * 2 * 3 ... * 10 = ', ProcessTheList(SomeFunction)); end. |
Un método: declarar con el objeto al final.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyMethod = procedure (const A: Integer) of object; TMyClass = class CurrentValue: Integer; procedure Add(const A: Integer); procedure Multiply(const A: Integer); procedure ProcessTheList(const M: TMyMethod); end; procedure TMyClass.Add(const A: Integer); begin CurrentValue := CurrentValue + A; end; procedure TMyClass.Multiply(const A: Integer); begin CurrentValue := CurrentValue * A; end; procedure TMyClass.ProcessTheList(const M: TMyMethod); var I: Integer; begin CurrentValue := 1; for I := 2 to 10 do M(I); end; var C: TMyClass; begin C := TMyClass.Create; try C.ProcessTheList(@C.Add); WriteLn('1 + 2 + 3 ... + 10 = ', C.CurrentValue); C.ProcessTheList(@C.Multiply); WriteLn('1 * 2 * 3 ... * 10 = ', C.CurrentValue); finally FreeAndNil(C); end; end. |
Tenga en cuenta que no puede pasar procedimientos/funciones globales como métodos. Son incompatibles. Si tiene que proporcionar una devolución de llamada de objeto, pero no desea crear una instancia de clase ficticia, puede pasar los métodos de clase como métodos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type TMyMethod = function (const A, B: Integer): Integer of object; TMyClass = class class function Add(const A, B: Integer): Integer class function Multiply(const A, B: Integer): Integer end; var M: TMyMethod; begin M := @TMyClass(nil).Add; M := @TMyClass(nil).Multiply; end; |
Desafortunadamente, debe escribir feo @TMyClass(nil).Add en lugar de solo @TMyClass.Add.
- Una (posiblemente) rutina local: declare with está anidada al final, y asegúrese de usar la directiva {$modeswitch nestedprocvars} para el código. Estos van de la mano con las rutinas locales (anidadas).
Genéricos
Una poderosa característica de cualquier lenguaje moderno. La definición de algo (típicamente, de una clase) se puede parametrizar con otro tipo. El ejemplo más típico es cuando necesitas crear un contenedor (una lista, diccionario, árbol, gráfico…): puedes definir una lista de tipo T, y luego especializarla para obtener instantáneamente una lista de enteros, una lista de cadenas , una lista de TMyRecord, etc.
Los genéricos en Pascal funcionan de forma muy similar a los genéricos en C++. Lo que significa que se «expanden» en el momento de la especialización, un poco como las macros (pero mucho más seguras que las macros; por ejemplo, los identificadores se resuelven en el momento de la definición genérica, no en la especialización, por lo que no puede «inyectar» nada inesperado). comportamiento al especializar el genérico). En efecto, esto significa que son muy rápidos (pueden optimizarse para cada tipo en particular) y funcionan con tipos de cualquier tamaño. Puede usar un tipo primitivo (entero, flotante), así como un registro, así como una clase al especializar un genérico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type generic TMyCalculator<T> = class Value: T; procedure Add(const A: T); end; procedure TMyCalculator.Add(const A: T); begin Value := Value + A; end; type TMyFloatCalculator = specialize TMyCalculator<Single>; TMyStringCalculator = specialize TMyCalculator<string>; var FloatCalc: TMyFloatCalculator; StringCalc: TMyStringCalculator; begin FloatCalc := TMyFloatCalculator.Create; try FloatCalc.Add(3.14); FloatCalc.Add(1); WriteLn('FloatCalc: ', FloatCalc.Value:1:2); finally FreeAndNil(FloatCalc); end; StringCalc := TMyStringCalculator.Create; try StringCalc.Add('something'); StringCalc.Add(' more'); WriteLn('StringCalc: ', StringCalc.Value); finally FreeAndNil(StringCalc); end; end. |
Los genéricos no se limitan a las clases, también puede tener funciones y procedimientos genéricos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; { Note: this example requires FPC 3.1.1 (will not compile with FPC 3.0.0 or older). } generic function Min<T>(const A, B: T): T; begin if A < B then Result := A else Result := B; end; begin WriteLn('Min (1, 0): ', specialize Min<Integer>(1, 0)); WriteLn('Min (3.14, 5): ', specialize Min<Single>(3.14, 5):1:2); WriteLn('Min (''a'', ''b''): ', specialize Min<string>('a', 'b')); end. |
Consulte también los Contenedores (listas, diccionarios) que usan genéricos sobre clases estándar importantes que usan genéricos.
Sobrecarga
Se permiten métodos (y funciones y procedimientos globales) con el mismo nombre, siempre que tengan parámetros diferentes. En tiempo de compilación, el compilador detecta cuál desea usar, sabiendo los parámetros que pasa.
De forma predeterminada, la sobrecarga utiliza el enfoque FPC, lo que significa que todos los métodos en un espacio de nombres dado (una clase o una unidad) son iguales y ocultan los otros métodos en espacios de nombres con menos prioridad. Por ejemplo, si define una clase con los métodos Foo(Integer) y Foo(string), y desciende de una clase con el método Foo(Float), entonces los usuarios de su nueva clase no podrán acceder al método Foo( Float) fácilmente (todavía pueden — si encasillan la clase a su tipo de antepasado). Para superar esto, utilice la palabra clave de sobrecarga.
Preprocesado
Puede usar directivas de preprocesador simples para:
- compilación condicional (código dependiendo de la plataforma, o algunos modificadores personalizados)
- incluir un archivo en otro
- también puede utilizar macros sin parámetros.
Tenga en cuenta que no se permiten macros con parámetros. En general, debe evitar usar el preprocesador… a menos que esté realmente justificado. El procesamiento previo ocurre antes del análisis, lo que significa que puede «romper» la sintaxis normal del lenguaje Pascal. Esta es una característica poderosa, pero también algo sucia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
{$mode objfpc}{$H+}{$J-} unit PreprocessorStuff; interface {$ifdef FPC} { This is only defined when compiled by FPC, not other compilers (like Delphi). } procedure Foo; {$endif} { Define a NewLine constant. Here you can see how the normal syntax of Pascal is "broken" by preprocessor directives. When you compile on Unix (includes Linux, Android, macOS), the compiler sees this: const NewLine = #10; When you compile on Windows, the compiler sees this: const NewLine = #13#10; On other operating systems, the code will fail to compile, because a compiler sees this: const NewLine = ; It's a *good* thing that the compilation fails in this case -- if you will have to port the program to an OS that is not Unix, not Windows, you will be reminded by a compiler to choose the newline convention on that system. } const NewLine = {$ifdef UNIX} #10 {$endif} {$ifdef MSWINDOWS} #13#10 {$endif} ; {$define MY_SYMBOL} {$ifdef MY_SYMBOL} procedure Bar; {$endif} {$define CallingConventionMacro := unknown} {$ifdef UNIX} {$define CallingConventionMacro := cdecl} {$endif} {$ifdef MSWINDOWS} {$define CallingConventionMacro := stdcall} {$endif} procedure RealProcedureName; CallingConventionMacro; external 'some_external_library'; implementation {$include some_file.inc} // $I is just a shortcut for $include {$I some_other_file.inc} end. |
Los archivos de inclusión tienen comúnmente la extensión .inc y se usan para dos propósitos:
- El archivo de inclusión solo puede contener otras directivas del compilador, que «configuran» su código fuente. Por ejemplo, podría crear un archivo myconfig.inc con estos contenidos:
1 2 3 4 5 6 7 |
{$mode objfpc} {$H+} {$J-} {$modeswitch advancedrecords} {$ifndef VER3} {$error This code can only be compiled using FPC version at least 3.x.} {$endif} |
Ahora puede incluir este archivo usando {$I myconfig.inc} en todas sus fuentes.
- El otro uso común es dividir una unidad grande en muchos archivos, y al mismo tiempo mantenerla en una sola unidad en lo que respecta a las reglas del idioma. No abuse de esta técnica — su primer instinto debe ser dividir una sola unidad en múltiples unidades, no dividir una sola unidad en múltiples archivos de inclusión. Sin embargo, esta es una técnica útil.
- Permite evitar la «explosión» del número de unidades, al mismo tiempo que mantiene cortos los archivos de código fuente. Por ejemplo, puede ser mejor tener una sola unidad con «controles de interfaz de usuario de uso común» que crear una unidad para cada clase de control de interfaz de usuario, ya que este último enfoque haría que la cláusula típica de «usos» fuera larga (ya que un código de interfaz de usuario típico dependen de un par de clases de interfaz de usuario). Pero colocar todas estas clases de interfaz de usuario en un solo archivo myunit.pas lo convertiría en un archivo largo, difícil de navegar, por lo que dividirlo en varios archivos de inclusión puede tener sentido.
- Permite tener una interfaz de unidad multiplataforma con implementación dependiente de la plataforma fácilmente. Básicamente puedes hacer
1 2 |
{$ifdef UNIX} {$I my_unix_implementation.inc} {$endif} {$ifdef MSWINDOWS} {$I my_windows_implementation.inc} {$endif} |
A veces, esto es mejor que escribir un código largo con muchos {$ifdef UNIX}, {$ifdef MSWINDOWS} mezclados con código normal (declaraciones de variables, implementación de rutinas). El código es más legible de esta manera. Incluso puede usar esta técnica de manera más agresiva, usando la opción de línea de comandos -Fi de FPC para incluir algunos subdirectorios solo para plataformas específicas. Luego puede tener muchas versiones del archivo de inclusión {$I my platform_specific_implementation.inc} y simplemente incluirlas, permitiendo que el compilador encuentre la versión correcta.
Registros
Record es solo un contenedor para otras variables. Es como una clase mucho más simplificada: no hay herencia ni métodos virtuales. Es como una estructura en lenguajes tipo C
Si usa la directiva {$modeswitch advancedrecords}, los registros pueden tener métodos y especificadores de visibilidad. En general, las características del idioma que están disponibles para las clases y que no rompen el diseño de memoria predecible simple de un registro, son posibles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{$mode objfpc}{$H+}{$J-} {$modeswitch advancedrecords} type TMyRecord = record public I, Square: Integer; procedure WriteLnDescription; end; procedure TMyRecord.WriteLnDescription; begin WriteLn('Square of ', I, ' is ', Square); end; var A: array [0..9] of TMyRecord; R: TMyRecord; I: Integer; begin for I := 0 to 9 do begin A[I].I := I; A[I].Square := I * I; end; for R in A do R.WriteLnDescription; end. |
En el Object Pascal moderno, su primer instinto debería ser diseñar una clase, no un registro, porque las clases están repletas de funciones útiles, como constructores y herencia.
Pero los registros siguen siendo muy útiles cuando necesita velocidad o un diseño de memoria predecible:
- Los registros no tienen ningún constructor o destructor. Simplemente define una variable de un tipo de registro. Tiene contenidos indefinidos (basura de memoria) al principio (excepto los tipos administrados automáticamente, como cadenas; se garantiza que se inicializarán para estar vacíos y se finalizarán para liberar el recuento de referencias). Por lo tanto, debe tener más cuidado cuando se trata de registros, pero le brinda cierta ganancia de rendimiento.
- Las matrices de registros son muy lineales en la memoria, por lo que son compatibles con la memoria caché.
- El diseño de la memoria de los registros (tamaño, relleno entre campos) está claramente definido en algunas situaciones: cuando solicita el diseño C o cuando utiliza un registro empaquetado. Esto es útil:
- para comunicarse con bibliotecas escritas en otros lenguajes de programación, cuando exponen una API basada en registros
- para leer y escribir archivos binarios
- para hacer trucos sucios de bajo nivel (como encasillar inseguro un tipo a otro, ser consciente de su representación de memoria).
- Los registros también pueden tener partes de casos, que funcionan como uniones en lenguajes similares a C. Permiten tratar la misma pieza de memoria como de un tipo diferente, según sus necesidades. Como tal, esto permite una mayor eficiencia de la memoria en algunos casos. Y permite más trucos inseguros sucios y de bajo nivel 🙂
Objetos al estilo antiguo
En los viejos tiempos, Turbo Pascal introdujo otra sintaxis para la funcionalidad de clase, utilizando la palabra clave de objeto. Es algo así como una mezcla entre el concepto de un disco y una clase moderna.
- Los objetos de estilo antiguo se pueden asignar/liberar, y durante esa operación puede llamar a su constructor/destructor.
- Pero también pueden declararse y usarse simplemente, como registros. Un registro simple o un tipo de objeto no es una referencia (puntero) a algo, son simplemente los datos. Esto los hace cómodos para datos pequeños, donde la asignación de llamadas / gratis sería molesta.
- Los objetos de estilo antiguo ofrecen herencia y métodos virtuales, aunque con pequeñas diferencias con las clases modernas. Tenga cuidado — sucederán cosas malas si intenta usar un objeto sin llamar a su constructor, y el objeto tiene métodos virtuales.
Se desaconseja utilizar los objetos de estilo antiguo en la mayoría de los casos. Las clases modernas proporcionan mucha más funcionalidad. Y cuando sea necesario, los registros (incluidos los registros avanzados) se pueden utilizar para el rendimiento. Estos conceptos suelen ser una mejor idea que los objetos de estilo antiguo.
Punteros
Puede crear un puntero a cualquier otro tipo. El puntero para escribir TMyRecord se declara como ^TMyRecord y, por convención, se llama PMyRecord. Este es un ejemplo tradicional de una lista enlazada de enteros usando registros:
1 2 3 4 5 6 |
type PMyRecord = ^TMyRecord; TMyRecord = record Value: Integer; Next: PMyRecord; end; |
Tenga en cuenta que la definición es recursiva (el tipo PMyRecord se define mediante el tipo TMyRecord, mientras que TMyRecord se define mediante PMyRecord). Se permite definir un tipo puntero a un tipo aún no definido, siempre que se resuelva dentro del mismo bloque de tipo.
Puede asignar y liberar punteros usando los métodos New/Dispose, o (más bajo nivel, no tipo seguro) métodos GetMem/FreeMem. Elimina la referencia del puntero (para acceder a las cosas señaladas por) agrega el operador ^ (por ejemplo, MyInteger := MyPointerToInteger^). Para realizar la operación inversa, que consiste en obtener un puntero de una variable existente, se antepone el operador @ (por ejemplo, MyPointerToInteger := @MyInteger).
También hay un tipo de puntero sin tipo, similar a void* en lenguajes tipo C. Es completamente inseguro y puede encasillarse en cualquier otro tipo de puntero.
Recuerde que una instancia de clase también es, de hecho, un puntero, aunque no requiere ningún operador ^ o @ para usarlo. Una lista enlazada usando clases es ciertamente posible, sería simplemente esto:
1 2 3 4 5 |
type TMyClass = class Value: Integer; Next: TMyClass; end; |
Sobrecarga de operadores
Puede anular el significado de muchos operadores de idiomas, por ejemplo, para permitir la adición y multiplicación de sus tipos personalizados. Como esto:
1 2 3 4 5 6 7 8 9 10 11 12 |
{$mode objfpc}{$H+}{$J-} uses StrUtils; operator* (const S: string; const A: Integer): string; begin Result := DupeString(S, A); end; begin WriteLn('bla' * 10); end. |
También puede anular operadores en clases. Dado que generalmente crea nuevas instancias de sus clases dentro de la función del operador, la persona que llama debe recordar liberar el resultado.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyClass = class MyInt: Integer; end; operator* (const C1, C2: TMyClass): TMyClass; begin Result := TMyClass.Create; Result.MyInt := C1.MyInt * C2.MyInt; end; var C1, C2: TMyClass; begin C1 := TMyClass.Create; try C1.MyInt := 12; C2 := C1 * C1; try WriteLn('12 * 12 = ', C2.MyInt); finally FreeAndNil(C2); end; finally FreeAndNil(C1); end; end. |
También puede anular operadores en registros. Esto suele ser más fácil que sobrecargarlos para las clases, ya que la persona que llama no tiene que lidiar con la gestión de la memoria.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyRecord = record MyInt: Integer; end; operator* (const C1, C2: TMyRecord): TMyRecord; begin Result.MyInt := C1.MyInt * C2.MyInt; end; var R1, R2: TMyRecord; begin R1.MyInt := 12; R2 := R1 * R1; WriteLn('12 * 12 = ', R2.MyInt); end. |
Para registros, se recomienda usar {$modeswitch advancedrecords} y anular operadores como operadores de clase dentro del registro. Esto permite utilizar clases genéricas que dependen de la existencia de algún operador (como TFPGList, que depende de la disponibilidad del operador de igualdad) con dichos registros. De lo contrario, no se encontraría la definición «global» de un operador (no dentro del registro) (porque no está disponible en el código que implementa TFPGList), y no podría especializar una lista como special TFPGList.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
{$mode objfpc}{$H+}{$J-} {$modeswitch advancedrecords} uses SysUtils, FGL; type TMyRecord = record MyInt: Integer; class operator+ (const C1, C2: TMyRecord): TMyRecord; class operator= (const C1, C2: TMyRecord): boolean; end; class operator TMyRecord.+ (const C1, C2: TMyRecord): TMyRecord; begin Result.MyInt := C1.MyInt + C2.MyInt; end; class operator TMyRecord.= (const C1, C2: TMyRecord): boolean; begin Result := C1.MyInt = C2.MyInt; end; type TMyRecordList = specialize TFPGList<TMyRecord>; var R, ListItem: TMyRecord; L: TMyRecordList; begin L := TMyRecordList.Create; try R.MyInt := 1; L.Add(R); R.MyInt := 10; L.Add(R); R.MyInt := 100; L.Add(R); R.MyInt := 0; for ListItem in L do R := ListItem + R; WriteLn('1 + 10 + 100 = ', R.MyInt); finally FreeAndNil(L); end; end. |