Contenido
- 1 Características de las clases avanzadas
- 2 Privado y estrictamente privado
- 3 Más cosas dentro de clases y clases anidadas
- 4 Métodos de clase
- 5 Referencias de clase
- 6 Métodos de clase estática
- 7 Propiedades de clase y variables
- 8 Ayudantes de clase (Helpers)
- 9 Constructores virtuales, destructores
- 10 Una excepción en el constructor.
Características de las clases avanzadas
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.
Privado y estrictamente privado
El especificador de visibilidad privada significa que el campo (o método) no es accesible fuera de esta clase. Pero permite una excepción: todo el código definido en la misma unidad puede romper esto y acceder a campos y métodos privados. Un programador de C++ diría que en Pascal todas las clases dentro de una sola unidad son amigas. Esto suele ser útil y no rompe la encapsulación, ya que está limitado a una unidad. Sin embargo, si crea unidades más grandes, con muchas clases (que no están estrechamente integradas entre sí), es más seguro usar la privacidad estricta. Significa que el campo (o método) no es accesible fuera de este período de clase. Sin excepciones. De manera similar, hay visibilidad protegida (visible para los descendientes o amigos en la misma unidad) y protección estricta (visible para los descendientes, punto).
Más cosas dentro de clases y clases anidadas
Puede abrir una sección de constantes (const) o tipos (tipo) dentro de una clase. De esta manera, incluso puede definir una clase dentro de una clase. Los especificadores de visibilidad funcionan como siempre, en particular, la clase anidada puede ser privada (no visible para el mundo exterior), lo que suele ser útil.
Tenga en cuenta que para declarar un campo después de una constante o tipo, deberá abrir un bloque var.
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 |
type TMyClass = class private type TInternalClass = class Velocity: Single; procedure DoSomething; end; var FInternalClass: TInternalClass; public const DefaultVelocity = 100.0; constructor Create; destructor Destroy; override; end; constructor TMyClass.Create; begin inherited; FInternalClass := TInternalClass.Create; FInternalClass.Velocity := DefaultVelocity; FInternalClass.DoSomething; end; destructor TMyClass.Destroy; begin FreeAndNil(FInternalClass); inherited; end; { note that method definition is prefixed with "TMyClass.TInternalClass" below. } procedure TMyClass.TInternalClass.DoSomething; begin end; |
Métodos de clase
Estos son métodos a los que puede llamar con una referencia de clase (TMyClass), no necesariamente una instancia de clase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type TEnemy = class procedure Kill; class procedure KillAll; end; var E: TEnemy; begin E := TEnemy.Create; try E.Kill; finally FreeAndNil(E) end; TEnemy.KillAll; end; |
Tenga en cuenta que pueden ser virtuales — tiene sentido, y a veces es muy útil, cuando se combinan con referencias de clase.
Los métodos de clase también pueden estar limitados por los especificadores de visibilidad, como privado o protegido. Al igual que los métodos regulares.
Tenga en cuenta que un constructor siempre actúa como un método de clase cuando se llama de forma normal (MyInstance := TMyClass.Create(…);). Aunque también es posible llamar a un constructor desde dentro de la propia clase, como un método normal, y luego actúa como un método normal. Esta es una característica útil para «encadenar» constructores, cuando un constructor (por ejemplo, sobrecargado para tomar un parámetro entero) hace algún trabajo y luego llama a otro constructor (por ejemplo, sin parámetros).
Referencias de clase
La referencia de clase le permite elegir la clase en tiempo de ejecución, por ejemplo, para llamar a un método de clase o constructor sin conocer la clase exacta en tiempo de compilación. Es un tipo declarado como clase de TMyClass.
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 |
type TMyClass = class(TComponent) end; TMyClass1 = class(TMyClass) end; TMyClass2 = class(TMyClass) end; TMyClassRef = class of TMyClass; var C: TMyClass; ClassRef: TMyClassRef; begin // Obviously you can do this: C := TMyClass.Create(nil); FreeAndNil(C); C := TMyClass1.Create(nil); FreeAndNil(C); C := TMyClass2.Create(nil); FreeAndNil(C); // In addition, using class references, you can also do this: ClassRef := TMyClass; C := ClassRef.Create(nil); FreeAndNil(C); ClassRef := TMyClass1; C := ClassRef.Create(nil); FreeAndNil(C); ClassRef := TMyClass2; C := ClassRef.Create(nil); FreeAndNil(C); end; |
Las referencias de clase se pueden combinar con métodos de clase virtual. Esto da un efecto similar al uso de clases con métodos virtuales — el método real que se ejecutará se determina en tiempo de ejecución.
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 |
type TMyClass = class(TComponent) class procedure DoSomething; virtual; abstract; end; TMyClass1 = class(TMyClass) class procedure DoSomething; override; end; TMyClass2 = class(TMyClass) class procedure DoSomething; override; end; TMyClassRef = class of TMyClass; var C: TMyClass; ClassRef: TMyClassRef; begin ClassRef := TMyClass1; ClassRef.DoSomething; ClassRef := TMyClass2; ClassRef.DoSomething; { And this will cause an exception at runtime, since DoSomething is abstract in TMyClass. } ClassRef := TMyClass; ClassRef.DoSomething; end; |
Si tiene una instancia y desea obtener una referencia a su clase (no la clase declarada, sino la clase descendiente final utilizada en su construcción), puede usar la propiedad ClassType. El tipo declarado de ClassType es TClass, que significa clase de TObject. A menudo, puede encasillarlo de manera segura en algo más específico, cuando sabe que la instancia es algo más específico que TObject.
En particular, puede usar la referencia ClassType para llamar a métodos virtuales, incluidos los constructores virtuales. Esto le permite crear un método como Clone que construye una instancia de la clase de tiempo de ejecución exacta del objeto actual. Puede combinarlo con Cloning: TPersistent.Assign para tener un método que devuelva un clon recién construido de la instancia actual.
Recuerda que solo funciona cuando el constructor de tu clase es virtual. Por ejemplo, se puede usar con los descendientes estándar de TComponent, ya que todos deben anular el constructor virtual TComponent.Create(AOwner: TComponent).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
type TMyClass = class(TComponent) procedure Assign(Source: TPersistent); override; function Clone(AOwner: TComponent): TMyClass; end; TMyClassRef = class of TMyClass; function TMyClass.Clone(AOwner: TComponent): TMyClass; begin // This would always create an instance of exactly TMyClass: //Result := TMyClass.Create(AOwner); // This can potentially create an instance of TMyClass descendant: Result := TMyClassRef(ClassType).Create(AOwner); Result.Assign(Self); end; |
Métodos de clase estática
Para comprender los métodos de clase estáticos, debe comprender cómo funcionan los métodos de clase normales (descritos en las secciones anteriores). Internamente, los métodos de clase normales reciben una referencia de clase de su clase (se pasa a través de un primer parámetro del método oculto e implícitamente agregado). Incluso se puede acceder a esta referencia de clase explícitamente usando la palabra clave Self dentro del método de clase. Por lo general, es algo bueno: esta referencia de clase le permite llamar a métodos de clases virtuales (a través de la tabla de métodos virtuales de la clase).
Si bien esto es bueno, hace que los métodos de clase normales sean incompatibles cuando se asignan a un puntero de procedimiento global. Es decir, esto no compilará:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{$mode objfpc}{$H+}{$J-} type TMyCallback = procedure (A: Integer); TMyClass = class class procedure Foo(A: Integer); end; class procedure TMyClass.Foo(A: Integer); begin end; var Callback: TMyCallback; begin // Error: TMyClass.Foo not compatible with TMyCallback Callback := @TMyClass(nil).Foo; end. |
Nota: En el modo Delphi, podría escribir TMyClass.Foo en lugar de un feo TMyClass(nil).Foo en el ejemplo anterior. Es cierto que TMyClass.Foo se ve mucho más elegante y el compilador también lo verifica mejor. Usar TMyClass(nil).Foo es un truco… desafortunadamente, necesario (por ahora) en el modo ObjFpc que se presenta a lo largo de este libro.
En cualquier caso, la asignación de TMyClass.Foo a la devolución de llamada anterior aún fallaría en el modo Delphi, exactamente por las mismas razones.
El ejemplo anterior no se compila porque Callback es incompatible con el método de clase Foo. Y es incompatible porque internamente el método de clase tiene ese parámetro implícito oculto especial para pasar una referencia de clase.
Una forma de corregir el ejemplo anterior es cambiar la definición de TMyCallback. Funcionará si se trata de una devolución de llamada de método, declarada como TMyCallback = procedimiento (A: Integer) of object;. Pero a veces, no es deseable.
Aquí viene el método de clase estática. Es, en esencia, solo un procedimiento/función global, pero su espacio de nombres está limitado dentro de la clase. No tiene ninguna referencia de clase implícita (y por lo tanto, no puede ser virtual y no puede llamar a métodos de clase virtual). Por el lado positivo, es compatible con las devoluciones de llamadas normales (sin objetos). Así que esto funcionará:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{$mode objfpc}{$H+}{$J-} type TMyCallback = procedure (A: Integer); TMyClass = class class procedure Foo(A: Integer); static; end; class procedure TMyClass.Foo(A: Integer); begin end; var Callback: TMyCallback; begin Callback := @TMyClass.Foo; end. |
Propiedades de clase y variables
Una propiedad de clase es una propiedad a la que se puede acceder a través de una referencia de clase (no necesita una instancia de clase).
Es una analogía bastante directa de una propiedad regular (ver Propiedades). Para una propiedad de clase, define un getter y/o un setter. Pueden hacer referencia a una variable de clase o a un método de clase estático.
Una variable de clase es, lo adivinó, como un campo normal, pero no necesita una instancia de clase para acceder a ella. En efecto, es como una variable global, pero con el espacio de nombres limitado a la clase contenedora. Se puede declarar dentro de la sección class var de la clase. Alternativamente, se puede declarar siguiendo la definición de campo normal con la palabra clave static.
Y un método de clase estática es como un procedimiento/función global, pero con el espacio de nombres limitado a la clase contenedora. Para obtener más información sobre los métodos de clase estáticos en la sección anterior, consulte Métodos de clase estáticos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{$mode objfpc}{$H+}{$J-} type TMyClass = class strict private // Alternative: // FMyProperty: Integer; static; class var FMyProperty: Integer; class procedure SetMyProperty(const Value: Integer); static; public class property MyProperty: Integer read FMyProperty write SetMyProperty; end; class procedure TMyClass.SetMyProperty(const Value: Integer); begin Writeln('MyProperty changes!'); FMyProperty := Value; end; begin TMyClass.MyProperty := 123; Writeln('TMyClass.MyProperty is now ', TMyClass.MyProperty); end. |
Ayudantes de clase (Helpers)
El método es solo un procedimiento o función dentro de una clase. Desde el exterior de la clase, la llama con una sintaxis especial MyInstance.MyMethod(…). Después de un tiempo te acostumbras a pensar que si quiero hacer una acción Acción en la instancia X, escribo X.Action(…)
.
Pero a veces, necesita implementar algo que conceptualmente es una acción en la clase TMyClass sin modificar el código fuente de TMyClass. A veces es porque no es su código fuente y no quiere cambiarlo. A veces se debe a las dependencias — agregar un método como Render a una clase como TMy3DObject parece una idea sencilla, pero tal vez la implementación base de la clase TMy3DObject debería mantenerse independiente del código de renderizado. Sería mejor «mejorar» una clase existente, agregarle funcionalidad sin cambiar su código fuente.
Una forma sencilla de hacerlo es crear un procedimiento global que tome una instancia de TMy3DObject como su primer parámetro.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type TMy3DObjectHelper = class helper for TMy3DObject procedure Render(const Color: TColor); end; procedure TMy3DObjectHelper.Render(const Color: TColor); var I: Integer; begin { note that we access ShapesCount, Shape without any qualifiers here } for I := 0 to ShapesCount - 1 do RenderMesh(Shape[I].Mesh, Color); end; |
El concepto más general es «type helper». Al usarlos, puede agregar métodos incluso a tipos primitivos, como enteros o enumeraciones. También puede agregar «ayudantes de registro» a (lo adivinó…) registros. Consulte http://lists.freepascal.org/fpc-announce/2013-February/000587.html.
Constructores virtuales, destructores
El nombre del destructor siempre es Destroy, es virtual (ya que puede llamarlo sin conocer la clase exacta en tiempo de compilación) y sin parámetros.
El nombre del constructor es por convención Create.
Puede cambiar este nombre, aunque tenga cuidado con esto: si define CreateMy, siempre redefina también el nombre Create, de lo contrario, el usuario aún puede acceder al constructor Create del antepasado, sin pasar por su constructor CreateMy.
En el TObject base no es virtual, y al crear descendientes eres libre de cambiar los parámetros. El nuevo constructor ocultará el constructor en el ancestro (nota: no ponga aquí sobrecarga, a menos que quiera romperlo).
En los descendientes de TComponent, debe anular su constructor Create (AOwner: TComponent);. Para la funcionalidad de transmisión, para crear una clase sin conocer su tipo en el momento de la compilación, es muy útil tener constructores virtuales (consulte Referencias de clase más arriba).
Una excepción en el constructor.
¿Qué sucede si ocurre una excepción durante un constructor? La línea:
1 |
X := TMyClass.Create; |
no se ejecuta hasta el final en este caso, no se puede asignar X, entonces, ¿quién limpiará después de una clase parcialmente construida?
La solución de Object Pascal es que, en caso de que ocurra una excepción dentro de un constructor, se llama al destructor. Esta es una razón por la que su destructor debe ser robusto, lo que significa que debería funcionar en cualquier circunstancia, incluso en una instancia de clase creada a medias. Por lo general, esto es fácil si libera todo de forma segura, como FreeAndNil.
También tenemos que depender en tales casos de que se garantice que la memoria de la clase se ponga a cero justo antes de que se ejecute el código del constructor. Entonces sabemos que al principio, todas las referencias de clase son nulas, todos los números enteros son 0 y así sucesivamente.
Entonces, a continuación, funciona sin fugas de memoria:
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 |
{$mode objfpc}{$H+}{$J-} uses SysUtils; type TGun = class end; TPlayer = class Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end; constructor TPlayer.Create; begin inherited; Gun1 := TGun.Create; raise Exception.Create('Raising an exception from constructor!'); Gun2 := TGun.Create; end; destructor TPlayer.Destroy; begin { in case since the constructor crashed, we can have Gun1 <> nil and Gun2 = nil now. Deal with it. ...Actually, in this case, FreeAndNil deals with it without any additional effort on our side, because FreeAndNil checks whether the instance is nil before calling its destructor. } FreeAndNil(Gun1); FreeAndNil(Gun2); inherited; end; begin try TPlayer.Create; except on E: Exception do WriteLn('Caught ' + E.ClassName + ': ' + E.Message); end; end. |