Fenner
A linguagem

defer — limpeza junto de quem abre

O comando adiado até o fim do bloco: abrir e fechar lado a lado, em ordem inversa, por qualquer saída.

O que é e por que existe#

Todo recurso que se abre precisa ser fechado: arquivo, conexão, trava, objeto temporário. O try…finally resolve, mas separa as duas pontas: o Free fica dezenas de linhas abaixo do Create, e cada novo recurso aninha mais um try. O defer coloca o fechamento logo depois da abertura, e o compilador garante que ele roda quando o bloco termina.

Fenner
LFile := TResource.Create('arquivo');
defer LFile.Free;                  // roda no fim do bloco, aconteça o que acontecer
LSocket := TResource.Create('conexão');
defer LSocket.Free;                // registrado depois, roda ANTES

É açúcar sobre o try…finally que já existia: A; defer B; C; equivale a A; try C; finally B; end;. Por isso valem as regras do finally.

Sintaxe e regras#

Fenner
defer <comando>;                   // um comando simples
defer begin ... end;               // ou um bloco
  • Quando roda: ao terminar o bloco que contém o defer, por qualquer saída: fim normal, Exit, Break/Continue que saem do bloco, ou exceção.
  • Ordem: vários defer rodam em ordem inversa à da escrita (LIFO), o que espelha "abrir A, abrir B, fechar B, fechar A".
  • Argumentos avaliados no fim: como num finally, e não na hora do defer (como em Go). Para congelar um valor, copie-o para uma variável local antes.
  • Result já está gravado: com Exit(V), o valor é decidido antes de o defer rodar; um defer LValue := 99 não muda o que foi devolvido.
  • Em laço: o defer do corpo roda ao fim de cada volta, inclusive quando a volta termina em Continue ou Break.
  • Variáveis gerenciadas: um objeto local (ARC) é solto depois dos defer do bloco, mesmo que tenha sido declarado depois do defer.
rc-defer.fxp
program Defer;
// Demo: `defer` adia um comando até a saída do bloco atual.
// Várias saídas possíveis (fim normal, Exit, Continue, Break, exceção) -> a limpeza roda sempre.

// Os defers rodam em ordem inversa (LIFO): o último registrado roda primeiro.
// Isso casa com "abrir A, abrir B ... fechar B, fechar A".
procedure Process(AFail: Bool8);
begin
  WriteLn('abre recurso A');
  defer WriteLn('fecha recurso A');   // registrado primeiro, roda por último
  WriteLn('abre recurso B');
  defer WriteLn('fecha recurso B');   // registrado depois, roda primeiro
  if AFail then
    raise 'falhou no meio';           // mesmo com exceção, os dois defers rodam
  WriteLn('trabalho feito');
end procedure;

// O defer roda DEPOIS de Result ser gravado: o valor devolvido já foi decidido.
function Value: Int32;
var
  LValue: Int32;
begin
  LValue := 1;
  defer LValue := 99;                 // não altera o que Exit devolve
  Exit(LValue);
end function;

var
  I: Int32;
begin
  WriteLn('--- sem falha');
  Process(False);
  WriteLn('--- com falha');
  try
    Process(True);
  except
    on E: Exception do WriteLn('capturou: ', E.Message);
  end try;
  WriteLn('--- Result já gravado: ', Value);
  WriteLn('--- defer em laço (roda a cada volta, inclusive no Continue)');
  for I := 1 to 3 do
  begin
    defer WriteLn('  fim da volta ', I);
    if I = 2 then
      Continue;
    WriteLn('  volta ', I);
  end for;
end.
Saída
--- sem falha
abre recurso A
abre recurso B
trabalho feito
fecha recurso B
fecha recurso A
--- com falha
abre recurso A
abre recurso B
fecha recurso B
fecha recurso A
capturou: falhou no meio
--- Result já gravado: 1
--- defer em laço (roda a cada volta, inclusive no Continue)
  volta 1
  fim da volta 1
  fim da volta 2
  volta 3
  fim da volta 3

Demo 40. Os dois defer rodam em ordem inversa, também com exceção; Value devolve 1 mesmo com o defer; no laço o defer roda a cada volta.

O demo 36 mostra a mesma ideia com objetos de verdade, e a ordem em relação ao ARC (primeiro os defer, depois os objetos locais são soltos):

rc-defer-recursos.fxp
program Defer;
// Demo 36: defer - "faça isto quando o bloco terminar", junto de onde abriu.
//
// Em vez de separar "abrir" e "fechar" por dezenas de linhas (ou usar um
// try/finally aninhado para cada recurso), escreva o fechamento logo depois
// da abertura. Os defer rodam em ordem INVERSA (o último registrado roda
// primeiro) e rodam mesmo se uma exceção ou Exit interromper o bloco.

type
  TResource = class
  private
    FName: UString;
  public
    constructor Create(const AName: UString);
    destructor Destroy; override;
  end class;

constructor TResource.Create(const AName: UString);
begin
  FName := AName;
  WriteLn('  abriu ', FName);
end constructor;

destructor TResource.Destroy;
begin
  WriteLn('  fechou ', FName);
  inherited;
end destructor;

procedure Work(AFail: Bool8);
var
  LFile, LSocket: TResource;
begin
  WriteLn('início (falhar = ', AFail, ')');
  LFile := TResource.Create('arquivo');
  defer WriteLn('  defer 1: registrado primeiro, roda por último');
  LSocket := TResource.Create('conexão');
  defer WriteLn('  defer 2: registrado depois, roda primeiro');
  if AFail then
    raise 'algo deu errado no meio';
  WriteLn('  corpo terminou normalmente');
  // ao sair: primeiro rodam os defer (2, depois 1); só então as variáveis
  // locais são soltas e os objetos (ARC) fecham.
end procedure;

var
  I: Int32;
begin
  Work(False);
  try
    Work(True);
  except
    on E: Exception do WriteLn('capturou: ', E.Message);
  end try;

  // em laços, o defer roda ao fim de CADA volta (também com Continue/Break)
  for I := 1 to 3 do
  begin
    defer WriteLn('  fim da volta ', I);
    if I = 2 then
      Continue;
    WriteLn('volta ', I);
  end for;
end.
Saída
início (falhar = FALSE)
  abriu arquivo
  abriu conexão
  corpo terminou normalmente
  defer 2: registrado depois, roda primeiro
  defer 1: registrado primeiro, roda por último
  fechou arquivo
  fechou conexão
início (falhar = TRUE)
  abriu arquivo
  abriu conexão
  defer 2: registrado depois, roda primeiro
  defer 1: registrado primeiro, roda por último
  fechou arquivo
  fechou conexão
capturou: algo deu errado no meio
volta 1
  fim da volta 1
  fim da volta 2
volta 3
  fim da volta 3

Demo 36. Os defer fecham na ordem inversa; só depois a contagem de referência solta os objetos.

Armadilhas e erros típicos#

  • Corpo direto de if/while/for: if X then defer ...; não tem bloco para adiar até o fim dele. O erro é FE0651: o defer só vale diretamente dentro de um begin…end; escreva o corpo como begin … end.
  • Break, Continue e Exit dentro do comando adiado são recusados, pela mesma razão que não se sai de um finally.
  • defer é palavra contextual: só vale no começo de um comando seguido de outro comando. defer := 1, defer(x) e defer.Foo continuam sendo identificadores comuns. Consequência: defer; sem comando não tem erro próprio; sai "identificador não declarado".
  • Só em minúsculas, como as demais palavras da linguagem.

Veja também#

  • Nulos e exceções, para o try…finally por baixo.
  • Memória, para a ordem em que o ARC solta os objetos.
  • Demos 40-defer e 36-defer; manual do compilador, seção "defer".