Fenner
A linguagem

readonly e init

Campos que só o construtor grava e properties init: objetos imutáveis depois de criados.

O que é e por que existe#

Muitos objetos nascem prontos e nunca mudam: um ponto, um nome, uma data de nascimento. Garantir isso por convenção falha com o tempo. readonly (campo) e init (escritor de property) fazem o compilador garantir: só o construtor grava, e qualquer outra tentativa é erro de compilação. Quem não usa nenhum dos dois gera o mesmo código e as mesmas mensagens de antes.

Campo readonly#

Fenner
type
  TPoint = class
  private
    readonly FX: Int32;
    readonly FY: Int32;
    FHits: Int32;                  // um campo comum
  public
    constructor Create(AX, AY: Int32);
    property X: Int32 read FX;
  end class;

constructor TPoint.Create(AX, AY: Int32);
begin
  FX := AX;                        // ok: construtor da própria classe
  Self.FY := AY;                   // ok, com Self explícito
end constructor;

readonly vem antes do nome (ou da lista de nomes) do campo, em classe ou em record: readonly A, B: Int32;. O fc não tem inicializador de campo na declaração (FX: Int32 = 5; não existe), então o construtor é o único lugar de dar valor.

Quem pode gravar: só um construtor da própria classe (a que declara o campo), sobre o seu Self. Dentro dele tudo vale (atribuir, repetir, Inc, passar a var/out, @FX).

Situação (todas fora do construtor, ou em construtor de descendente, ou sobre outra instância)Código
atribuir ao campo ou a parte dele (FRec.A := V, FFix[1] := V)FE2450
passar o campo a var/outFE2451
@FXFE2452
Inc/Dec/Include/SetLength/Delete/Insert/Finalize/New/ReadLn/FillChar sobre o campoFE2453

O que readonly protege#

O campo, não o objeto apontado. Num campo de classe, interface, vetor dinâmico ou ponteiro, readonly protege a referência: FNode.Valor := 1 e FLista.Add(X) continuam valendo; FNode := Outro e SetLength(FLista, N) são recusados. Já num campo de record ou de vetor estático, o valor mora dentro dele, e FRec.A := 1 é recusado fora do construtor.

Property init#

init é um escritor como write, mas só vale dentro de um construtor da classe dona (ou de um descendente), sobre o Self:

Fenner
property Name: UString read FName init FName;      // init sobre um campo readonly
property Nick: UString read FNick init SetNick;    // ou sobre um procedure de um parâmetro

O alvo é um campo readonly ou um procedure (FE2455 nos outros casos). write FX sobre campo readonly é recusado (FE2456): use init. init e write na mesma property: FE2457; class property … init: FE2458; init duas vezes: FE2459. Um descendente pode republicar a property e herda o init.

rc-readonly-init.fxp
program ReadonlyInit;
// Demo 25: campos "readonly" e properties "init" (imutabilidade).
//
// Um campo "readonly" só pode ser gravado DENTRO DO CONSTRUTOR da classe.
// Depois que o objeto nasce, ele não muda mais. A property "init" é o
// escritor que só vale dentro de um construtor.
// (Tentar gravar fora do construtor é erro de compilação: FE2450.)

type
  TPerson = class
  private
    readonly FName: UString;      // gravável só no construtor
    readonly FBirthYear: Int32;
    FVisits: Int32;               // campo comum: muda à vontade
  public
    constructor Create(const AName: UString; ABirthYear: Int32);
    procedure Visit;
    function Age(ACurrentYear: Int32): Int32;
    property Name: UString read FName init FName;     // "init": escrita só em construtor
    property BirthYear: Int32 read FBirthYear;        // somente leitura
    property Visits: Int32 read FVisits;
  end class;

constructor TPerson.Create(const AName: UString; ABirthYear: Int32);
begin
  Name := AName;                 // via property init (permitido aqui)
  FBirthYear := ABirthYear;      // direto no campo readonly (permitido aqui)
end constructor;

procedure TPerson.Visit;
begin
  Inc(FVisits);                  // campo comum, sem restrição
  // FBirthYear := 0;            // ERRO FE2450 se descomentar: fora do construtor
end procedure;

function TPerson.Age(ACurrentYear: Int32): Int32;
begin
  Result := ACurrentYear - FBirthYear;
end function;

var
  LPerson: TPerson;
begin
  LPerson := TPerson.Create('Ana', 1990);
  LPerson.Visit;
  LPerson.Visit;
  WriteLn(LPerson.Name, ' nasceu em ', LPerson.BirthYear);
  WriteLn('idade em 2026: ', LPerson.Age(2026));
  WriteLn('visitas: ', LPerson.Visits);
  // LPerson.Name := 'Bia';      // ERRO FE2454 se descomentar: property é "init"
end.
Saída
Ana nasceu em 1990
idade em 2026: 36
visitas: 2

Demo 25. Name via property init e FBirthYear direto no construtor; as duas linhas que dariam FE2450 e FE2454 estão comentadas no código.

Para ver os erros, descomente as duas linhas. Foi o que o compilador respondeu ao gravar FX num método e P.X := 5 numa property init:

texto
Erro FE2450: não é possível gravar no campo readonly "FX": só um construtor de "TPoint" o grava
Erro FE2454: a property "X" é "init": só um construtor de "TPoint" (ou de um descendente) pode atribuí-la

Armadilhas#

  • readonly e init são contextuais e só em minúsculas: readonly: Int32; como nome de campo, variável ou método continua valendo.
  • Records têm o campo readonly gravado no construtor de instância do record. Um record sem construtor só nasce zerado ou recebe a cópia de outro.
  • Package: o flag readonly ainda não é gravado no .fxd, então um tipo vindo de package perde a restrição.
  • Chamar um método de record que muda o Self sobre um campo record readonly não é recusado (não existe "método const").
  • Genéricos (TBox<T>) aceitam as duas formas.

Veja também#