Fenner
A linguagem

Classes e helpers

Classes, visibilidade, métodos virtuais, properties, herança com inherited e os helpers que acrescentam métodos a tipos existentes.

Declarando uma classe#

classes.fxp
program Classes;

type
  TAnimal = class(TObject)
  private
    FName: UString;
  public
    constructor Create(const AName: UString);
    procedure Speak; virtual; abstract;
    function Describe: UString; virtual;
    property Name: UString read FName write FName;
  end class;

  TDog = class(TAnimal)
  public
    procedure Speak; override;
  end class;

  TCat = class(TAnimal)
  public
    procedure Speak; override;
    function Describe: UString; override;
  end class;

constructor TAnimal.Create(const AName: UString);
begin
  FName := AName;
end constructor;

function TAnimal.Describe: UString;
begin
  Exit('animal ' + FName);
end function;

procedure TDog.Speak;
begin
  WriteLn(Name, ': au au');
end procedure;

procedure TCat.Speak;
begin
  WriteLn(Name, ': miau');
end procedure;

function TCat.Describe: UString;
begin
  Exit('gato ' + inherited Describe);
end function;

var
  Pets: array[0..1] of TAnimal;
  Pet: TAnimal;
begin
  Pets[0] := TDog.Create('Rex');
  Pets[1] := TCat.Create('Mimi');
  for Pet in Pets do
  begin
    Pet.Speak;                 // despacho virtual
    WriteLn('  ', Pet.Describe);
    if Pet is TCat then
      WriteLn('  (é um gato)');
  end for;
end.
Saída
Rex: au au
  animal Rex
Mimi: miau
  gato animal Mimi
  (é um gato)
Fenner
type
  TAnimal = class(TObject)
  private
    FName: UString;
  public
    constructor Create(const AName: UString);
    procedure Speak; virtual; abstract;
    property Name: UString read FName write FName;
  end class;

O fecho pode ser end; ou o qualificado end class;. O ancestral é opcional (class sozinho herda de TObject), e uma classe pode implementar interfaces na mesma lista: class(TObject, IShape).

Visibilidade#

private, protected, public, published e strict (com private e protected) valem dentro da classe. O corpo de um método não leva visibilidade: quem manda é a declaração da classe.

Métodos virtuais#

PalavraEfeito
virtualo método pode ser sobrescrito; o despacho é pela VMT
overridesobrescreve um virtual do ancestral
abstractsem corpo; os descendentes devem sobrescrever
reintroduceesconde de propósito um virtual de mesmo nome
finalnum método: ninguém sobrescreve; numa classe: final class, a árvore para nela
overloado conjunto do descendente se junta ao do ancestral
staticmétodo sem Self (static function X)

Para o otimizador, duas marcas ajudam muito:

Fenner
type
  TSale = class(TObject)
  public
    FCustomer: exact TCustomer;     // guarda EXATAMENTE TCustomer, nunca um descendente
  end class;

  TSupplier = final class(TObject);  // a árvore para aqui
  TBase = class abstract(TObject);   // classe abstrata: só um descendente concreto é instanciado

exact T deixa o compilador resolver a chamada virtual em compilação (devirtualização), e a devirtualização de métodos sem override já é feita sozinha. Ela pode ser desligada por {$config devirtualization off}.

Construtores, destrutores e class constructor#

constructor Create(...) e destructor Destroy; override; funcionam como no Delphi. Um class constructor roda uma vez, no arranque, e é podado se o tipo morrer na eliminação de código não usado. inherited chama o ancestral.

inherited, em três degraus#

FormaChega em
inherited;o mesmo método, um degrau acima
inherited Nome(…)outro método, um degrau acima
inherited TClasseA.Nomeum degrau escolhido da cadeia

A terceira forma existe porque uma cadeia de override só deixa chegar ao pai imediato, e às vezes o que se quer é o de mais longe. A classe citada tem de estar na cadeia de quem escreve.

is, as e metaclasses#

Obj is TCat testa o tipo; Obj as TCat converte com verificação. class of T é uma metaclasse: um tipo cujos valores são classes (TClass é class of TObject) e que aceita constantes tipadas.

Properties#

Fenner
property Celsius: Float64 read FCelsius write FCelsius;          // por campo
property Fahrenheit: Float64 read GetFahrenheit write SetFahrenheit; // por método
property Reading[const AIndex: Int32]: Int32 read GetReading write SetReading;
properties.fxp
program Properties;

type
  TTemperature = class(TObject)
  private
    FCelsius: Float64;
    FHistory: array[0..3] of Int32;
    function GetFahrenheit: Float64;
    procedure SetFahrenheit(const AValue: Float64);
    function GetReading(const AIndex: Int32): Int32;
    procedure SetReading(const AIndex: Int32; const AValue: Int32);
  public
    // leitura e escrita por campo
    property Celsius: Float64 read FCelsius write FCelsius;
    // leitura e escrita por método
    property Fahrenheit: Float64 read GetFahrenheit write SetFahrenheit;
    // property indexada: os índices vão na frente do valor
    property Reading[const AIndex: Int32]: Int32 read GetReading write SetReading;
  end class;

function TTemperature.GetFahrenheit: Float64;
begin
  Exit(FCelsius * 9 / 5 + 32);
end function;

procedure TTemperature.SetFahrenheit(const AValue: Float64);
begin
  FCelsius := (AValue - 32) * 5 / 9;
end procedure;

function TTemperature.GetReading(const AIndex: Int32): Int32;
begin
  Exit(FHistory[AIndex]);
end function;

procedure TTemperature.SetReading(const AIndex: Int32; const AValue: Int32);
begin
  FHistory[AIndex] := AValue;
end procedure;

var
  T: TTemperature;
begin
  T := TTemperature.Create;
  T.Celsius := 100;
  WriteLn(T.Fahrenheit);
  T.Fahrenheit := 32;
  WriteLn(T.Celsius);
  T.Reading[2] := 99;
  WriteLn(T.Reading[2]);
end.
Saída
212
0
99
  • Numa property indexada, os índices são parâmetros de verdade e vão para o acessor na frente do valor: o getter recebe só os índices, o setter recebe os índices e o valor por último. Ligar uma property indexada direto a um campo não existe.
  • property Name: UString DefReadWrite; (sem os acessores) sintetiza os acessores sobre o campo F<Nome>; há também DefRead e DefWrite. A visibilidade dos sintetizados é ajustável ({$config DefGetVisibility private}, DefField, …).
  • Uma property de interface é açúcar puro sobre os métodos dela.

Helpers#

Um helper for acrescenta métodos a um tipo que já existe — uma classe, um record ou até um tipo básico:

helpers.fxp
program Helpers;

type
  // acrescenta métodos a um tipo que já existe (aqui, um tipo básico)
  TInt32Helper = helper for Int32
  public
    function Double: Int32;
    function IsEven: Bool8;
    static function FromText(const S: UString): Int32;
  end helper;

  TGreeter = class(TObject)
  public
    procedure Hello(const S: UString);
  end class;

  // o helper GANHA do tipo quando os dois declaram o mesmo nome; o escondido
  // continua alcançável por inherited
  TGreeterHelper = helper for TGreeter
  public
    procedure Hello(const S: UString);
  end helper;

function TInt32Helper.Double: Int32;
begin
  Exit(Self * 2);               // Self é o próprio Int32
end function;

function TInt32Helper.IsEven: Bool8;
begin
  Exit(Self mod 2 = 0);
end function;

static function TInt32Helper.FromText(const S: UString): Int32;
begin
  Exit(UStrToInt32(S));
end function;

procedure TGreeter.Hello(const S: UString);
begin
  WriteLn('tipo:   ', S);
end procedure;

procedure TGreeterHelper.Hello(const S: UString);
begin
  WriteLn('helper: ', S);
  inherited Hello(S);           // chega no método do tipo estendido
end procedure;

var
  N: Int32;
  G: TGreeter;
begin
  N := 21;
  WriteLn(N.Double, ' ', N.IsEven, ' ', TInt32Helper.FromText('40'));
  G := TGreeter.Create;
  G.Hello('olá');
end.
Saída
42 FALSE 40
helper: olá
tipo:   olá

Algumas regras que o diferenciam do Delphi:

  • É uma construção só: o Delphi tem class helper for e record helper for; aqui basta helper for, porque o que muda entre um tipo e outro (ter Self de instância, ter VMT) já está no tipo estendido. Por dentro o helper é um record sem campos.
  • O helper ganha do tipo. Se os dois declaram o mesmo nome com a mesma assinatura, quem responde é o helper. É o contrário do Delphi, e é deliberado: um helper existe para substituir o comportamento de um tipo que não se pode editar.
  • Com overload, os dois conjuntos se juntam em vez de um esconder o outro.
  • O que o helper esconde não some: inherited alcança o método do tipo estendido, como numa classe alcança o do ancestral.
  • Um método static do helper não recebe Self e é chamado pelo nome do helper (TInt32Helper.FromText('40')), não pelo do tipo básico.

Atributos#

attribute Nome(params); declara a forma de um atributo, e [Nome(args)] decora tipos, campos, properties e métodos. Eles vão para o RTTI externo (<saída>.rtti.json, ligado pela opção Rtti), não para o binário, e alimentam os meta modules.

attributes.fxp
program Attributes;

// Atributos: lidos pelo compilador, conferidos contra a declaração e gravados
// só no RTTI externo (<saída>.rtti.json). Nada disso entra no binário, e é
// por isso que um meta module consegue lê-los em compilação.

attribute DbTable(const TableName: LString);
attribute DbField(const FieldName: LString);

type
  [DbTable('CUSTOMERS')]
  TCustomer = class(TObject)
  private
    [DbField('ID')]
    FId: Int32;
  public
    // DefReadWrite sintetiza os acessores sobre o campo F<Nome>
    [DbField('NAME')]
    property Name: UString DefReadWrite;
  end class;

var
  C: TCustomer;
begin
  C := TCustomer.Create;
  C.Name := 'Ana';
  WriteLn(C.Name);
end.
Saída
Ana