Fenner
A linguagem

Interfaces e records

Interfaces com contagem de referências, records com construtores e operadores, records gerenciados e a parte variante.

Interfaces#

Uma interface descreve um contrato; várias classes o implementam, e o código usa a interface sem saber qual é a classe.

interfaces.fxp
program Interfaces;

type
{$config InterfaceExtendedSyntax on}
  IShape = interface(IInterface)
    function Area: Int32; virtual;
    function Name: UString; virtual;
  end interface;

  TSquare = class(TObject, IShape)
  private
    FSide: Int32;
  public
    constructor Create(const ASide: Int32);
    function Area: Int32; => Exit(FSide * FSide);
    function Name: UString; => Exit('quadrado');
  end class;

  TRectangle = class(TObject, IShape)
  private
    FWidth, FHeight: Int32;
  public
    constructor Create(const AWidth, AHeight: Int32);
    function Area: Int32; => Exit(FWidth * FHeight);
    function Name: UString; => Exit('retângulo');
  end class;

constructor TSquare.Create(const ASide: Int32);
begin
  FSide := ASide;
end constructor;

constructor TRectangle.Create(const AWidth, AHeight: Int32);
begin
  FWidth := AWidth;
  FHeight := AHeight;
end constructor;

procedure Show(const AShape: IShape);
begin
  WriteLn(AShape.Name, ': área ', AShape.Area);
end procedure;

begin
  // a referência de interface mantém o objeto vivo (contagem de referências)
  Show(TSquare.Create(4));
  Show(TRectangle.Create(3, 5));
end.
Saída
quadrado: área 16
retângulo: área 15
  • Toda interface herda de IInterface (fora do módulo System). O despacho é pela ITable da classe.
  • Uma referência de interface mantém o objeto vivo (contagem de referências): Show(TSquare.Create(4)) não vaza nem precisa de Free.
  • Uma interface pode ser declarada adiantada (IWriter = interface;), com o corpo depois: é o que resolve duas interfaces que se citam. Uma adiantada que nunca ganha corpo é erro, porque quem a citou receberia uma referência que compila e não tem método nenhum.
  • Interfaces de COM têm a ABI própria (safecall, stdcall), e a RTL as usa para o Windows (IStream, IUnknown…).

Records#

Um record é um valor: atribuir copia. Pode ter métodos, construtores e operadores, e herdar de outro record.

records.fxp
program Records;

type
  TPoint = record
    X, Y: Int32;
  public
    constructor Create(const AX, AY: Int32);
    class operator Add(const A, B: TPoint): TPoint;
      => Exit(TPoint.Create(A.X + B.X, A.Y + B.Y));
    class operator Equal(const A, B: TPoint): Bool8;
      => Exit((A.X = B.X) and (A.Y = B.Y));
  end record;

  // record gerenciado: Initialize quando passa a existir, Finalize quando deixa de existir
  TGuard = managed record
    Id: Int32;
    class operator Initialize(out Dest: TGuard); => Dest.Id := 7;
    class operator Finalize(var Dest: TGuard); => WriteLn('  Finalize, Id=', Dest.Id);
  end record;

constructor TPoint.Create(const AX, AY: Int32);
begin
  X := AX;
  Y := AY;
end constructor;

procedure Scope;
var
  G: TGuard;
begin
  WriteLn('  Initialize deu Id=', G.Id);
  G.Id := 42;
end procedure;

var
  A, B, C: TPoint;
begin
  A := TPoint.Create(1, 2);     // constrói direto no destino: sem alocação
  B := TPoint.Create(10, 20);
  C := A + B;                   // o operador Add do record
  WriteLn('C = (', C.X, ', ', C.Y, ')');
  WriteLn('C = A + B? ', C = TPoint.Create(11, 22));

  WriteLn('entrando no escopo:');
  Scope;
  WriteLn('saiu do escopo');
end.
Saída
C = (11, 22)
C = A + B? TRUE
entrando no escopo:
  Initialize deu Id=7
  Finalize, Id=42
saiu do escopo
Fenner
type
  TPoint = record
    X, Y: Int32;
  public
    constructor Create(const AX, AY: Int32);
    class operator Add(const A, B: TPoint): TPoint;
  end record;
  • TPoint.Create(…) constrói no destino: nenhuma alocação. Um construtor pode delegar para outra sobrecarga.
  • Um record pode herdar de outro (TDerived = record(TBase)): os campos vêm depois do SizeOf do ancestral, e a passagem é por referência, não por valor.
  • packed record remove o preenchimento, e end align(N) fixa um alinhamento mínimo (um piso, não um teto).

Operadores#

Cada operador é um class operator, um método de classe static: não há Self, os dois operandos são os parâmetros, e não precisam ser do mesmo tipo.

GrupoOperadores
De valorAdd Subtract Multiply Divide Equal NotEqual LessThan LessThanOrEqual GreaterThan GreaterThanOrEqual …
De destinoAssign Increment Decrement — escrevem no destino em vez de devolver valor; o primeiro operando é var
De vidaInitialize Finalize
ConversãoImplicit Explicit (a busca exata vence a frouxa)
OutrosDefault

Os operadores do ancestral valem para o descendente.

Records gerenciados#

Um managed record tem tempo de vida próprio: Initialize quando a variável passa a existir e Finalize quando deixa de existir (no fim do escopo, de um vetor, de um campo, ou do programa, se for global). Um record comum que contém um campo gerenciado passa a ser gerenciado por tabela, e a inicialização e a finalização descem até ele. É assim que Variant, OleVariant e os anuláveis (?T) são feitos.

Parte variante#

Os campos de uma parte variante se sobrepõem, como uma union do C:

record-variant.fxp
program RecordVariant;

type
  PPair^, TPair = record      // o ponteiro declarado junto do tipo
  public
    Tag: Int32;
    case Int32 of             // parte variante: os campos se sobrepõem
      0: (AsInt: Int32);
      1: (Bytes: array[0..3] of UInt8);
    end case;
  end record;

var
  R: TPair;
  P: PPair;
begin
  R.Tag := 1;
  R.AsInt := $01020304;
  WriteLn('bytes: ', R.Bytes[0], ' ', R.Bytes[3]);   // x86 é little-endian
  WriteLn('SizeOf(TPair) = ', SizeOf(TPair));

  P := @R;
  WriteLn('P^.Tag = ', P^.Tag);
end.
Saída
bytes: 4 1
SizeOf(TPair) = 8
P^.Tag = 1

A parte variante é fechada com end case; (o qualificador é conferido pelo compilador). Um rótulo de case pode ser uma expressão constante, e é a forma pela qual as constantes de GUID são escritas. O tipo ponteiro pode ser declarado junto do record, com PPair^, TPair = record.