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#
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/out | FE2451 |
@FX | FE2452 |
Inc/Dec/Include/SetLength/Delete/Insert/Finalize/New/ReadLn/FillChar sobre o campo | FE2453 |
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:
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âmetroO 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.
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.Ana nasceu em 1990
idade em 2026: 36
visitas: 2Demo 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:
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í-laArmadilhas#
readonlyeinitsã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
readonlygravado no construtor de instância do record. Um record sem construtor só nasce zerado ou recebe a cópia de outro. - Package: o flag
readonlyainda 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
Selfsobre um campo recordreadonlynão é recusado (não existe "método const"). - Genéricos (
TBox<T>) aceitam as duas formas.
Veja também#
- Classes, with de expressão.
- Demo
25-readonly-init; manualreadonly.md.