Algunas diferencias entre el modo FPC ObjFpc y modo Delphi (y FPC en modo Delphi)
Nota: Este artículo es una traducción de https://github.com/modern-pascal/modern-pascal-introduction/wiki/Some-differences-betwen-FPC-ObjFpc-mode-and-Delphi-(and-FPC-Delphi-mode) escrito por Michalis Kamburelis
Contenido
- 1 Introducción
- 2 ¿Cuáles son las diferencias? Acompañado de algunas reflexiones personales (de Michalis), a veces con un sesgo, sobre ‘qué es mejor’.
- 3 En el modo ObjFpc, no está permitido tener nombres de campos/variables/parámetros que entren en conflicto, incluso entre descendientes y ancestros.
- 4 Tratando a los punteros como arrays, sin usar ^ (desreferenciación)
- 5 El String por defecto es ShortString en modo ObjFpc, probablemente querrás cambiarlo a AnsiString.
- 6 Genéricos
Introducción
El compilador FPC ofrece algunos «modos de sintaxis». Se pueden elegir mediante:
- Directivas en el código de Pascal (como por ejemplo:
{$mode objfpc}
,{$mode delphi}
). - O por la línea de comandos en el compilador FPC (
-Mobjfpc
,-Mdelphi
). - O en los ajustes del proyecto en Lazarus (en el archivo .lpi; aplicavable si compilas desde el IDE Lazarus).
- O usando la opción
-Mxxx
en<compiler_options>
dentro deCastleEngineManifest.xml
,si compilas usando el editor CGE o la herramienta CGE build (ver https://castle-engine.io/project_manifest#_compiler_options_and_paths ).
Existen algunos modos de sintaxis en FPC, pero en esta página nos centraremos en dos que son los más populares: la sintaxis ObjFpc y la sintaxis Delphi. Ofrecen 100% las mismas capacidades y son muy compatibles… pero no absolutamente compatibles, existen pequeñas diferencias que describimos a continuación.
Delphi no tiene este concepto (aunque también tiene algunas opciones que afectan a la sintaxis, mencionamos algunas de ellas más abajo).
Como sus nombres sugieren, el «modo Delphi» de FPC es muy compatible con el Delphi real.
El «modo ObjFpc» de FPC es un poco más recomendado por los desarrolladores de FPC (aunque esto puede depender de a quién le preguntes 🙂 ). De forma más práctica, las nuevas unidades creadas por Lazarus tienen {$mode objfpc}{$H+} y los nuevos proyectos de Lazarus comienzan con la configuración «modo ObjFpc». Por lo tanto, los desarrolladores de Lazarus recomiendan y usan por defecto el modo ObjFpc.
En Castle Game Engine, seguimos a Lazarus, por lo que usamos el modo ObjFpc por defecto para tus proyectos CGE, si se compilan con FPC. Aunque puedes cambiarlo para tus proyectos: simplemente añade -Mdelphi en las opciones del compilador en CastleEngineManifest.xml. Mostramos este ejemplo en https://castle-engine.io/project_manifest#_compiler_options_and_paths .
¿Cuáles son las diferencias? Acompañado de algunas reflexiones personales (de Michalis), a veces con un sesgo, sobre ‘qué es mejor’.
Variables a procedimientos
Las variables a procedimientos (punteros a procedimientos) tienen un sintaxis un poco diferentes.
Por ejemplo, puedes escribir:
procedure TMyView.ButtonClick(Sender: TObject);
begin
end;
procedure TMyView.Start;
begin
inherited;
OnClick := @ButtonClick; // this is only OK in FPC ObjFpc
end;
Por otro lado, Delphi, decidió que ButtonClick, sin @, es un dirección válida. En efecto, esta línea fallará al compilar:
OnClick := @ButtonClick; // this is only OK in FPC ObjFpc
(ya que en Delphi significa que intentas tomar una dirección de la dirección de ButtonClick
, por lo que sería la dirección de una variable temporal, lo cual no tiene sentido)
Solamente compila así:
OnClick := ButtonClick; // this is only OK in Delphi or FPC Delphi mode
¿Cuál es la diferencia?
- El enfoque ObjFpc de FPC (Free Pascal Compiler) es útil para hacer que algunas expresiones sean menos ambiguas. Por ejemplo, ¿qué significa si
OnMyEvent = MyFunction
entonces…?, cuando los eventos se definen comofunction:
Integer
(una función que devuelve un entero): ¿Esto compara los eventos asignados, o los llama? Se complica aún más cuando la función devuelve la dirección de otra función (comofunction: TNotifyEvent
). En el modo ObjFpc de FPC, estas expresiones son fáciles de entender porque «sin el símbolo @, no puedes tomar la dirección de un procedimiento, así que la expresión anterior llama aMyFunction
«. - Por lo que sé, la ventaja de la «sintaxis de Delphi» es simplemente la brevedad. Puedes olvidarte del símbolo @, y la mayoría de las expresiones son obvias para el compilador, es decir, puede distinguir cuándo quieres llamar a una función y cuándo quieres tomar la dirección de una función.
Mira nuestra prueba en https://github.com/michaliskambi/modern-pascal-introduction/blob/master/code-samples/delphi_and_fpc_differences/compare_function_pointers.dpr para ver algunas construcciones que, en mi humilde opinión, resultan realmente extrañas en Delphi/FPC en modo Delphi, debido a la decisión de Delphi de omitir el @
. Pero también es cierto que en el «código real y práctico» no te encuentras con estos casos tan a menudo
¿Qué hacer en tu código?
En el código y los ejemplos de CGE, decidimos simplemente «vivir con ello» y escribir {$ifdef FPC}@{$endif}
donde fuera necesario. Nos gusta la claridad adicional que aporta el modo ObjFpc de FPC al requerir @
. Además, muchos usuarios de FPC/Lazarus están más acostumbrados al modo ObjFpc, ya que es el modo predeterminado configurado en los nuevos proyectos de Lazarus, así que nos adherimos a él. Así que escribimos…
OnClick := {$ifdef FPC}@{$endif} ButtonClick;
Para ser perfectamente válidos, en realidad deberíamos escribir…
OnClick := {$ifdef FPC_OBJFPC}@{$endif} ButtonClick;.
Pero para simplificar, decidimos asumir que «cuando CGE se compila con FPC, sabemos que se compila en modo FPC ObjFpc, ya que lo configuramos nosotros mismos tanto en la línea de comandos como en castleconf.inc
.
También puedes optar por un enfoque diferente: usar el «modo Delphi» con FPC. Para ello, añade -Mdelphi
en las opciones del compilador en CastleEngineManifest.xml (la configuración del proyecto de Lazarus también tiene una opción para cambiar el modo). Consulta la sección «6.11. Opciones y rutas del compilador» en https://castle-engine.io/project_manifest#_compiler_options_and_paths , donde mostramos y discutimos explícitamente el uso de -Mdelphi
.
Entonces tendrás un código más consistente entre FPC y Delphi.
A continuación, se presentan más argumentos y discusiones sobre por qué el modo FPC ObjFpc es una buena idea:
Considera escribir esto, en Delphi o en modo FPC Delphi:
if X = Y then ...
¿Qué hace esto si X e Y son variables de tipo procedural? En modo Delphi (ya sea con {$mode delphi}
en FPC o en el propio Delphi), esta es una pregunta difícil. Surge del hecho de que en Pascal, llamas a una función sin parámetros sin usar paréntesis, es decir, sin el ()
. Si X e Y apuntan a funciones sin parámetros que devuelven, por ejemplo, enteros, entonces if X = Y then
en realidad las llamará y comparará sus resultados. Solo en otros casos se compararán realmente los punteros a las funciones.
Prueba: compare_function_pointers.dpr
. Es un programa Pascal válido, tanto para FPC como para Delphi. Sin embargo, el if X = Y then
hace algo diferente.
Para asegurarte de que comparas punteros a funciones en Delphi, puedes añadir el operador de dirección, como:
if @X = @Y then ...
Lo cual es feo, porque en este caso el @
no obtiene la dirección de X
(aunque «@X
» se define exactamente como «obtener la dirección de la variable X
» en el caso de otros tipos en Pascal). Simplemente indica que realmente queremos el puntero a la función.
Es aún más divertido si realmente quieres obtener «una dirección del puntero a la función», ya que entonces tienes que escribir:
@@X
Lo cual también carece de sentido en circunstancias normales. @X
suele ser un resultado temporal, no un «l-value» (un valor al que se le puede asignar algo), ¿cómo se le puede asignar algo?
En modo ObjFpc, todo es más consistente. X y @ se comportan igual que para otros tipos. La única regla nueva es: «cuando llamas a una variable procedural que contiene una función sin parámetros, lo haces como en C: le añades ()
«. Esto significa que llamar a X no parece tan «Pascal», ya que tienes que añadir paréntesis:
X()
… Pero todo es más natural. En el modo FPC ObjFpc esto:
if X = Y then
Compara punteros a funciones, cuando X e Y son, por ejemplo, del tipo function: Pointer
. Compara intuitivamente el contenido de X e Y, sin importar si X, Y son enteros, punteros a funciones o punteros a funciones sin parámetros. Además:
@X
Ahora es siempre la dirección de X (sin importar si X es de tipo Integer
o un puntero a función). La asignación de una variable de función a otra es normal:
X := Y
Y la asignación de una función real a X utiliza explícitamente el operador @
en el lado derecho, lo que significa que tomamos la dirección de la función:
X := @MyFunction;
En el modo ObjFpc, no está permitido tener nombres de campos/variables/parámetros que entren en conflicto, incluso entre descendientes y ancestros.
Esta regla rompe las reglas de alcance habituales de Pascal («se permiten conflictos cuando están en diferentes espacios de nombres, y el espacio de nombres más local es el que prevalece»), pero lo hace con un buen propósito: protegerte de ciertos errores. Imagina este código:
TMyClass = class
X: Integer;
procedure Foo(X: Integer);
end;
Esto está permitido en modo Delphi. Dentro de la implementación de Foo
, la resolución sigue las reglas normales de Pascal: el mismo nombre, X
, está permitido si está en ámbitos diferentes, y el ámbito más local tiene prioridad.
procedure TMyClass.Foo(X: Integer);
begin
X := 123; // changes the X parameter value, X field is untouched
end;
Esto es peligroso: si cambias la declaración de Foo
a Foo(Y: Integer)
, la implementación seguirá compilando… pero hará algo totalmente diferente, probablemente erróneo, ya que ahora estás cambiando el campo del objeto. Para evitar esto, ObjFpc lo prohíbe: los nombres de los parámetros deben ser diferentes a los nombres de los campos/métodos/propiedades de la clase. La clase TMyClass
anterior simplemente no compilará en modo ObjFpc; tendrás que cambiar el nombre del parámetro a algo como AX
, y probablemente recordarás usar AX
dentro de la implementación de Foo
. Si alguna vez cambias la declaración de Foo
, lo más probable es que obtengas un error de compilación hasta que corrijas todas las ocurrencias de AX
.
procedure TMyClass.Foo;
var
X: Integer
begin
...
end;
No está permitido.
También se aplica a los descendientes de TMyClass
; en TMyClassDescendant
tampoco puedes declarar X
(campos, variables locales, etc.).
Tratando a los punteros como arrays, sin usar ^
(desreferenciación)
En el modo FPC ObjFpc si tu tienes:
X: ^Integer;
Y usas:
X[2]
Entonces ObjFPC lo trata de forma similar a C: es como si X fuera un puntero a un array de Integer
, y quieres el segundo elemento. Así que:
Y: array [0..999] of Integer;
X: ^Integer;
...
X := @(Y[10]);
Assert(X^ = Y[10]);
Assert(X[0] = Y[10]);
Assert(X[2] = Y[12]);
TODO: restaurar mi demo: array_ptrs.lpr
En Delphi, esto no está permitido; no puedes tratar un puntero a TTT
como un array de TTT
.
Por otro lado, en Delphi hay un atajo diferente. Es decir, puedes simplemente omitir el ^
si el tipo TODO: restaurar mi demo: array_ptrs.lpr
En Delphi, esto no está permitido; no puedes tratar un puntero a TTT
como un array de TTT
.
Por otro lado, en Delphi hay un atajo diferente. Es decir, puedes simplemente omitir el ^
si el tipo desreferenciado ya es un array. Es decir, si X es un puntero y escribes:. Es decir, si X es un puntero y escribes:
X[2]
Entonces Delphi trata esto como:
X^[2]
Obviamente esto tiene sentido solamente si X es un puntero a una array como este:
type
TIntegers = array [0..999] of Integer;
var
X: ^TIntegers;
Un truco muy utilizado en Delphi es declarar un array «infinito» (de hecho, tan largo como sea posible compilar, de ahí el truco de MaxInt
), y usarlo como un tipo de puntero cómodo:
type
TMyRecord = record A, B, C: Integer; ... whatever else ... end;
TMyRecords = array [0..MaxInt div SizeOf(TMyRecord) - 1] of TMyRecord;
PMyRecords = ^TMyRecords;
var
MyRecords: PMyRecords;
... // and for example you can do:
MyRecords := GetMem(SizeOf(TMyRecord) * 10);
// in Delphi (real Delphi or delphi mode in FPC) you can do:
MyRecords[1].A := 123;
// in both Delphi mode and objfpc mode you can also do it without omitting ^:
MyRecords^[1].A := 123;
En el modo FPC ObjFpc, lo harías más bien así:
type
TMyRecord = record A, B, C: Integer; ... whatever else ... end;
PMyRecord = ^TMyRecord;
var
MyRecord: PMyRecord;
... // and for example you can do:
MyRecord := GetMem(SizeOf(TMyRecord) * 10);
// not writing ^ means that MyRecord is automatically treated as a pointer
// to an array of whatever it was declared.
MyRecord[1].A := 123;
Hay un pequeño «truco» en todo esto, un lugar donde tanto Delphi (el Delphi real o {$mode delphi}
en FPC) como el modo ObjFPC compilan, pero interpretan de forma diferente. Recuerda el ejemplo anterior del array infinito:
type
TMyRecord = record A, B, C: Integer; ... whatever else ... end;
TMyRecords = array [0..MaxInt div SizeOf(TMyRecord) - 1] of TMyRecord;
PMyRecords = ^TMyRecords;
var
MyRecords: PMyRecords;
... // and for example you can do:
MyRecords := GetMem(SizeOf(TMyRecord) * 10);
// in Delphi (real Delphi or {$mode delphi} in FPC) you can do:
MyRecords[1].A := 123;
El truco es que, de hecho, puedes usar MyRecords[1].A
también en modo ObjFPC. Compilará, pero significará algo que probablemente no quieres. MyRecords
es entonces tratado como un array de lo que sea que apunte… ¡así que MyRecords
es un puntero a un array de arrays TMyRecords
! MyRecords[1]
se refiere al segundo array. Definitivamente no es lo que querías, y está fuera de la memoria asignada.
Moraleja: no omitas el operador ^
. O presta atención a tu modo: tanto ObjFPC como Delphi tienen una idea diferente de cuándo puedes omitirlo y cómo se interpreta.
Nota: Versiones posteriores de Delphi introducen algo muy similar a FPC, {$pointermath on}
. Esto hace que Delphi se comporte bastante parecido a lo que hace FPC en este aspecto y, en mi humilde opinión, es una buena idea. Nuestro castleconf.inc
(usado en todas las unidades CGE) lo define ahora, te recomiendo que también lo uses en tus aplicaciones donde tenga sentido. Consulta https://docwiki.embarcadero.com/RADStudio/Sydney/en/Pointer_Math_(Delphi).
El String por defecto es ShortString
en modo ObjFpc, probablemente querrás cambiarlo a AnsiString
.
Finalmente, una pequeña diferencia que en realidad es una ligera molestia del modo ObjFPC. En ObjFPC, por defecto, s
tring
= ShortString
. Es por eso que casi siempre deberías hacer:
Al principio de tus fuentes, para usar la cadena moderna de Object Pascal.
No hagas esto, ya que casi nunca querrás lidiar con ShortString
y sus limitaciones en código moderno:
{$mode objfpc} // problamente no quieras hacer esto. Quizás quieras añadir {$H+} como está encima
En contraste, {$mode delphi}
cambia automáticamente string
para que sea un alias de la moderna AnsiString
por defecto. Así que, simplemente usando:
{$mode delphi}
Es suficiente. Por supuesto, también puedes usar {$mode delphi}{$H+}
, pero entonces el {$H+}
es simplemente superfluo.
ObjFPC se comporta así (no implica que string
sea AnsiString
) por compatibilidad con versiones anteriores. Ha habido discusiones sobre cambiar este comportamiento por defecto (y cambiar el modo por defecto de FPC), pero (hasta ahora) el argumento de la compatibilidad con versiones anteriores prevalece.
Ten en cuenta que el Delphi real, en sus versiones recientes, hace algo incluso diferente hoy en día: el String
por defecto es UnicodeString
, no ShortString
ni AnsiString
. En CGE, lo manejamos ajustándonos a los valores por defecto de ambos compiladores: la mayor parte del código debería usar simplemente String
, y estar preparado para que sea de 8 bits en FPC y de 16 bits en Delphi.
Genéricos
En FPC ObjFpc, debes usar las palabras clave generic
y specialization
, las cuales puedes omitir en Delphi (o en el modo FPC Delphi).
type
generic TMyCalculator<T> = class
Value: T;
procedure Add(const A: T);
end;
TMyFloatCalculator = specialize TMyCalculator<Single>;
(fuente: https://github.com/michaliskambi/modern-pascal-introduction/blob/master/code-samples/generics.lpr )
En FPC ObjFpc, debes usar las palabras clave generic
y specialization
, las cuales puedes omitir en Delphi (o en el modo FPC Delphi).
Esto es:
En Delphi, o en modo FPC Delphi, simplemente eliminas estas palabras clave. O las rodeas con {$ifdef FPC_OBJFPC}…{$endif}
. En Castle Game Engine, las rodeamos con {$ifdef FPC}…{$endif}
para simplificar y confiar en la suposición de que «cuando el engine es compilado por FPC, siempre está en modo ObjFpc».
Además, la implementación de FPC (tanto en modo ObjFpc como Delphi) es, en este momento, un poco menos estricta que la implementación de Delphi en lo que respecta a la comprobación de la corrección de tipos de los genéricos. Es decir:
- FPC verificará el 100% de la corrección en la especialización y permitirá definir algunos genéricos que pueden, o no, ser correctos.
- Delphi es más estricto en este sentido; se realizan más comprobaciones ya en la definición del genérico. El uso de restricciones genéricas es más a menudo necesario en Delphi.