Contenido
Interfaces
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.
Interfaces desnudas (CORBA)
Una interfaz declara una API, como una clase, pero no define la implementación. Una clase puede implementar muchas interfaces, pero solo puede tener una clase antecesora.
Puede transmitir una clase a cualquier interfaz que admita y luego llamar a los métodos a través de esa interfaz. Esto permite tratar de manera uniforme las clases que no descienden unas de otras, pero que aún comparten alguna funcionalidad común. Útil cuando una herencia de clase simple no es suficiente.
Las interfaces CORBA en Object Pascal funcionan de manera muy similar a las interfaces en Java (https://docs.oracle.com/javase/tutorial/java/concepts/interface.html) o C# (https://msdn.microsoft.com/en -us/library/ms173156.aspx).
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 57 58 59 60 61 62 63 64 65 66 67 68 |
{$mode objfpc}{$H+}{$J-} {$interfaces corba} uses SysUtils, Classes; type IMyInterface = interface ['{79352612-668B-4E8C-910A-26975E103CAC}'] procedure Shoot; end; TMyClass1 = class(IMyInterface) procedure Shoot; end; TMyClass2 = class(IMyInterface) procedure Shoot; end; TMyClass3 = class procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Shooting... '); I.Shoot; end; var C1: TMyClass1; C2: TMyClass2; C3: TMyClass3; begin C1 := TMyClass1.Create; C2 := TMyClass2.Create; C3 := TMyClass3.Create; try if C1 is IMyInterface then UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); // The "C3 is IMyInterface" below is false, // so "UseThroughInterface(C3 as IMyInterface)" will not execute. if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end; end. |
¿Por qué las interfaces (presentadas arriba) se llaman «CORBA»?
El nombre CORBA es desafortunado. Un mejor nombre sería interfaces desnudas (bare interfaces). Estas interfaces son una «función de lenguaje puro». Úselos cuando desee emitir varias clases como la misma interfaz, ya que comparten una API común. Si bien estos tipos de interfaces se pueden usar junto con la tecnología CORBA (Common Object Request Broker Architecture) (ver wikipedia sobre CORBA), no están vinculados a esta tecnología de ninguna manera.
¿Se necesita la declaración {$interfaces corba}?
Sí, porque por defecto creas interfaces COM. Esto se puede indicar explícitamente diciendo {$interfaces com}, pero por lo general no es necesario ya que es el estado predeterminado.
Y no aconsejo usar interfaces COM, especialmente si está buscando algo equivalente a las interfaces de otros lenguajes de programación. Las interfaces CORBA en Pascal son exactamente lo que espera si busca algo equivalente a las interfaces en C# y Java. Mientras que las interfaces COM traen características adicionales que posiblemente no desee.
Tenga en cuenta que la declaración {$interfaces xxx} solo afecta a las interfaces que no tienen ningún ancestro explícito (solo la palabra clave interface, no interface(ISomeAncestor)). Cuando una interfaz tiene un ancestro, tiene el mismo tipo que el ancestro, independientemente de la declaración {$interfaces xxx}.
¿Qué son las interfaces COM?
La interfaz COM es sinónimo de una interfaz que desciende de una interfaz especial IUnknown. Descendente de IUnknown:
Requiere que sus clases definan los métodos _AddRef y _ReleaseRef. La implementación adecuada de estos métodos puede administrar la vida útil de sus objetos utilizando el recuento de referencias.
Agrega el método QueryInterface.
Permite interactuar con la tecnología COM (Component Object Model).
¿Por qué aconseja no utilizar las interfaces COM?
Debido a que las interfaces COM «enredan» dos características que deberían no estar relacionadas (ortogonales) en mi opinión: herencia múltiple y conteo de referencias. Otros lenguajes de programación usan correctamente conceptos separados para estas dos características. Para ser claros: el conteo de referencias, que proporciona una gestión automática de la memoria (en situaciones simples, es decir, sin ciclos), es un concepto muy útil. Pero enredar esta característica con interfaces (en lugar de convertirlas en características ortogonales) no es limpio a mis ojos. Definitivamente no coincide con mis casos de uso.
- veces quiero enviar mis clases (que de otro modo no estarían relacionadas) a una interfaz común.
- A veces quiero administrar la memoria usando el enfoque de conteo de referencias.
- Quizás algún día quiera interactuar con la tecnología COM.
Pero todas estas son necesidades separadas y no relacionadas. Enredarlos en una característica de un solo idioma es contraproducente en mi experiencia. Causa problemas reales:
- Si quiero la característica de enviar clases a una API de interfaz común, pero no quiero el mecanismo de conteo de referencias (quiero liberar objetos manualmente), entonces las interfaces COM son problemáticas. Incluso cuando el conteo de referencias está deshabilitado por una implementación especial de _AddRef y _ReleaseRef, aún debe tener cuidado de nunca tener una referencia de interfaz temporal colgada, después de haber liberado la instancia de clase. Más detalles al respecto en la siguiente sección.
- Si quiero la función de conteo de referencias, pero no necesito una jerarquía de interfaz para representar algo diferente a la jerarquía de clases, entonces tengo que duplicar la API de mis clases en las interfaces. Creando así una sola interfaz para cada clase. Esto es contraproducente. Preferiría tener punteros inteligentes como una característica de idioma separada, no enredada con las interfaces (y afortunadamente, está llegando :).
Es por eso que aconsejo usar interfaces de estilo CORBA y la directiva {$interfaces corba}, en todo el código moderno que trata con interfaces.
Solo si necesita «recuento de referencias» y «herencia múltiple» al mismo tiempo, utilice las interfaces COM. Además, Delphi solo tiene interfaces COM por ahora, por lo que debe usar interfaces COM si su código debe ser compatible con Delphi.
¿Podemos tener un conteo de referencias con interfaces CORBA?
Sí. Simplemente agregue los métodos _AddRef / _ReleaseRef. No es necesario descender de la interfaz IUnknown. Aunque en la mayoría de los casos, si desea contar referencias con sus interfaces, también puede usar interfaces COM.
GUID de interfaces
Los GUID son los caracteres aparentemente aleatorios [‘{ABCD1234-…}’] que ve colocados en cada definición de interfaz. Sí, son solo al azar. Desafortunadamente, son necesarios.
Los GUID no tienen sentido si no planea integrarse con tecnologías de comunicación como COM o CORBA. Pero son necesarios, por razones de implementación. No se deje engañar por el compilador, que desafortunadamente le permite declarar interfaces sin GUID.
Sin los GUID (únicos), el operador is tratará sus interfaces de la misma manera. En efecto, devolverá verdadero si su clase admite cualquiera de sus interfaces. La función mágica Supports(ObjectInstance, IMyInterface) se comporta un poco mejor aquí, ya que se niega a compilarse para interfaces sin un GUID. Esto es cierto para las interfaces CORBA y COM, a partir de FPC 3.0.0.
Por lo tanto, para estar seguro, siempre debe declarar un GUID para su interfaz. Puede usar el generador GUID de Lazarus (Ctrl + Shift + G atajo en el editor). O puede utilizar un servicio en línea como https://www.guidgenerator.com/.
O puede escribir su propia herramienta para esto, usando las funciones CreateGUID y GUIDToString en RTL. Vea el ejemplo a continuación:
1 2 3 4 5 6 7 8 9 10 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; var MyGuid: TGUID; begin Randomize; CreateGUID(MyGuid); WriteLn('[''' + GUIDToString(MyGuid) + ''']'); end. |
Interfaces de conteo de referencia (COM)
Las interfaces COM traen dos características adicionales:
- integración con COM (una tecnología de Windows, también disponible en Unix a través de XPCOM, utilizada por Mozilla).
- conteo de referencias (lo que le brinda destrucción automática cuando todas las referencias de la interfaz quedan fuera del alcance).
Cuando utilice interfaces COM, debe conocer su mecanismo de destrucción automática y su relación con la tecnología COM.
En la práctica, esto significa que:
- Su clase necesita implementar métodos mágicos _AddRef, _Release y QueryInterface. O descender de algo que ya los implementa. Una implementación particular de estos métodos en realidad puede habilitar o deshabilitar la función de conteo de referencias de las interfaces COM (aunque deshabilitarla es algo peligroso — vea el siguiente punto).
- La clase estándar TInterfacedObject implementa estos métodos para habilitar el conteo de referencias.
- La clase estándar TComponent implementa estos métodos para deshabilitar el conteo de referencias.
- Debe tener cuidado de liberar la clase, cuando algunas variables de interfaz pueden hacer referencia a ella. Debido a que la interfaz se libera utilizando un método virtual (porque puede contarse por referencia, incluso si piratea el método _AddRef para que no se cuente por referencia…), no puede liberar la instancia del objeto subyacente siempre que alguna variable de interfaz pueda apuntar lo. Consulte «7.7 Recuento de referencias» en el manual de FPC (http://freepascal.org/docs-html/ref/refse47.html).
El enfoque más seguro para usar las interfaces COM es:
- aceptar el hecho de que son contados por referencia,
- derivar las clases apropiadas de TInterfacedObject,
- y evite usar la instancia de clase, en lugar de acceder a la instancia siempre a través de la interfaz, permitiendo que el conteo de referencias administre la desasignación.
Este es un ejemplo de uso de interfaz:
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 57 58 59 60 61 62 63 64 65 66 |
{$mode objfpc}{$H+}{$J-} {$interfaces com} uses SysUtils, Classes; type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end; TMyClass1 = class(TInterfacedObject, IMyInterface) procedure Shoot; end; TMyClass2 = class(TInterfacedObject, IMyInterface) procedure Shoot; end; TMyClass3 = class(TInterfacedObject) procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Shooting... '); I.Shoot; end; var C1: IMyInterface; // COM takes care of destruction C2: IMyInterface; // COM takes care of destruction C3: TMyClass3; // YOU have to take care of destruction begin C1 := TMyClass1.Create as IMyInterface; C2 := TMyClass2.Create as IMyInterface; C3 := TMyClass3.Create; try UseThroughInterface(C1); // no need to use "as" operator UseThroughInterface(C2); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); // this will not execute finally { C1 and C2 variables go out of scope and will be auto-destroyed now. In contrast, C3 is a class instance, not managed by an interface, and it has to be destroyed manually. } FreeAndNil(C3); end; end. |
Uso de interfaces COM con el recuento de referencias deshabilitado
Como se mencionó en la sección anterior, su clase puede descender de TComponent (o una clase similar como TNonRefCountedInterfacedObject y TNonRefCountedInterfacedPersistent) que deshabilita el conteo de referencias para las interfaces COM. Esto le permite usar interfaces COM y aún liberar la instancia de clase manualmente.
En este caso, debe tener cuidado de no liberar la instancia de clase cuando alguna variable de interfaz pueda hacer referencia a ella. Recuerde que cada Cx encasillado como IMyInterface también crea una variable de interfaz temporal, que puede estar presente incluso hasta el final del procedimiento actual. Por esta razón, el siguiente ejemplo usa un procedimiento UseInterfaces y libera las instancias de clase fuera de este procedimiento (cuando podemos estar seguros de que las variables de interfaz temporales están fuera del alcance).
Para evitar este lío, por lo general es mejor usar interfaces CORBA, si no desea contar referencias con sus interfaces.
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
{$mode objfpc}{$H+}{$J-} {$interfaces com} uses SysUtils, Classes; type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end; TMyClass1 = class(TComponent, IMyInterface) procedure Shoot; end; TMyClass2 = class(TComponent, IMyInterface) procedure Shoot; end; TMyClass3 = class(TComponent) procedure Shoot; end; procedure TMyClass1.Shoot; begin WriteLn('TMyClass1.Shoot'); end; procedure TMyClass2.Shoot; begin WriteLn('TMyClass2.Shoot'); end; procedure TMyClass3.Shoot; begin WriteLn('TMyClass3.Shoot'); end; procedure UseThroughInterface(I: IMyInterface); begin Write('Shooting... '); I.Shoot; end; var C1: TMyClass1; C2: TMyClass2; C3: TMyClass3; procedure UseInterfaces; begin if C1 is IMyInterface then //if Supports(C1, IMyInterface) then // equivalent to "is" check above UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); end; begin C1 := TMyClass1.Create(nil); C2 := TMyClass2.Create(nil); C3 := TMyClass3.Create(nil); try UseInterfaces; finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end; end. |
Typecasting interfaces
Esta sección se aplica a las interfaces CORBA y COM (sin embargo, tiene algunas excepciones explícitas para CORBA).
- La conversión a un tipo de interfaz mediante el operador as realiza una comprobación en tiempo de ejecución. Considere este código:
1 |
UseThroughInterface(Cx as IMyInterface); |
Funciona para todas las instancias C1, C2, C3 en los ejemplos de las secciones anteriores. Si se ejecuta, cometería un error en tiempo de ejecución en el caso de C3, que no implementa IMyInterface.
El uso como operador funciona de forma coherente, independientemente de si Cx se declara como una instancia de clase (como TMyClass2) o una interfaz (como IMyInterface2).
Sin embargo, no está permitido para las interfaces CORBA.
- En su lugar, puede convertir la instancia como una interfaz implícitamente:
1 |
UseThroughInterface(Cx); |
En este caso, el typecast debe ser válido en tiempo de compilación. Entonces esto se compilará para C1 y C2 (que se declaran como clases que implementan IMyInterface). Pero no compilará para C3.
En esencia, este typecast se ve y funciona igual que para las clases normales. Siempre que se requiera una instancia de una clase TMyClass, siempre puede usar allí una variable que se declara con una clase de TMyClass o descendiente de TMyClass. La misma regla se aplica a las interfaces. No hay necesidad de ningún typecast explícito en tales situaciones.
- También puede hacer typecast utilizando IMyInterface(Cx). Como esto:
1 |
UseThroughInterface(IMyInterface(Cx)); |
Por lo general, dicha sintaxis de typecast indica un typecast y no verificado. Sucederán cosas malas si envías contenido a una interfaz incorrecta. Y eso es cierto, si conviertes una clase a una clase, o una interfaz a una interfaz, usando esta sintaxis.
Aquí hay una pequeña excepción: si Cx se declara como una clase (como TMyClass2), entonces esta es una conversión tipo que debe ser válida en tiempo de compilación. Por lo tanto, transmitir una clase a una interfaz de esta manera es un typecast y rápido (verificado en tiempo de compilación).
Para probarlo todo, juegue con este código de ejemplo:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
{$mode objfpc}{$H+}{$J-} // {$interfaces corba} // note that "as" typecasts for CORBA will not compile uses Classes; type IMyInterface = interface ['{7FC754BC-9CA7-4399-B947-D37DD30BA90A}'] procedure One; end; IMyInterface2 = interface(IMyInterface) ['{A72B7008-3F90-45C1-8F4C-E77C4302AA3E}'] procedure Two; end; IMyInterface3 = interface(IMyInterface2) ['{924BFB98-B049-4945-AF17-1DB08DB1C0C5}'] procedure Three; end; TMyClass = class(TComponent, IMyInterface) procedure One; end; TMyClass2 = class(TMyClass, IMyInterface, IMyInterface2) procedure One; procedure Two; end; procedure TMyClass.One; begin Writeln('TMyClass.One'); end; procedure TMyClass2.One; begin Writeln('TMyClass2.One'); end; procedure TMyClass2.Two; begin Writeln('TMyClass2.Two'); end; procedure UseInterface2(const I: IMyInterface2); begin I.One; I.Two; end; procedure UseInterface3(const I: IMyInterface3); begin I.One; I.Two; I.Three; end; var My: IMyInterface; MyClass: TMyClass; begin My := TMyClass2.Create(nil); MyClass := TMyClass2.Create(nil); // This doesn't compile, since at compile-time it's unknown if My is IMyInterface2. // UseInterface2(My); // UseInterface2(MyClass); // This compiles and works OK. UseInterface2(IMyInterface2(My)); // This does not compile. Casting InterfaceType(ClassType) is checked at compile-time. // UseInterface2(IMyInterface2(MyClass)); // This compiles and works OK. UseInterface2(My as IMyInterface2); // This compiles and works OK. UseInterface2(MyClass as IMyInterface2); // This compiles, but will fail at runtime, with ugly "Access violation". // UseInterface3(IMyInterface3(My)); // This does not compile. Casting InterfaceType(ClassType) is checked at compile-time. // UseInterface3(IMyInterface3(MyClass)); // This compiles, but will fail at runtime, with nice "EInvalidCast: Invalid type cast". // UseInterface3(My as IMyInterface3); // This compiles, but will fail at runtime, with nice "EInvalidCast: Invalid type cast". // UseInterface3(MyClass as IMyInterface3); Writeln('Finished'); end. |
Sobre este documento
Copyright Michalis Kamburelis.
El código fuente de este documento está en AsciiDoc en https://github.com/michaliskambi/modern-pascal-introduction. Las sugerencias de correcciones y adiciones, parches y solicitudes de extracción siempre son bienvenidas 🙂 Puede comunicarse conmigo a través de GitHub o enviar un correo electrónico a michalis@castle-engine.io. Mi página de inicio es https://michalis.xyz/. Este documento está vinculado en la sección de Documentación del sitio web de Castle Game Engine https://castle-engine.io/.
Puede redistribuir e incluso modificar este documento libremente, bajo las mismas licencias que Wikipedia https://en.wikipedia.org/wiki/Wikipedia:Copyrights:
¡Gracias por leerlo!