Closures em detalhe
O que uma função anônima captura, por cópia ou por caixa, o caso do var/out, noescape e o objeto estático das closures sem captura.
O que é e por que existe#
A página de lambdas mostra a sintaxe. Esta explica o que acontece com as variáveis que uma função anônima usa: quem é dono delas, até quando vivem e o que é proibido. Um reference to guarda a função junto com o que ela capturou.
type
TAction = reference to procedure;
TTransform = reference to function(const AValue: Int32): Int32;
function MakeCounter(out AGet: TTransform): TAction;
var
LCount: Int32;
begin
LCount := 0;
AGet := function(const AValue: Int32): Int32; begin Result := LCount + AValue; end;
Result := procedure; begin Inc(LCount); end;
end function;O contador e o leitor enxergam a mesma LCount, que continua viva depois que MakeCounter retorna.
program Closures;
// Demo: funções anônimas e closures. `reference to` é o tipo "referência para rotina"
// que aceita tanto uma rotina comum quanto uma função anônima que CAPTURA variáveis.
uses System.Remainder;
type
TAction = reference to procedure;
TTransform = reference to function(const AValue: Int32): Int32;
// A variável local LCount é capturada: o contador (Result) e o leitor (AGet) enxergam
// a MESMA variável, que continua viva depois que MakeCounter retorna.
function MakeCounter(out AGet: TTransform): TAction;
var
LCount: Int32;
begin
LCount := 0;
AGet := function(const AValue: Int32): Int32; begin Result := LCount + AValue; end;
Result := procedure; begin Inc(LCount); end;
end function;
// Uma "fábrica de funções": cada chamada captura o seu próprio AFactor.
function MakeMultiplier(AFactor: Int32): TTransform;
begin
Result := function(const AValue: Int32): Int32; begin Result := AValue * AFactor; end;
end function;
// Parâmetro `const` gerenciado (cadeia) também é capturado com segurança.
function MakeGreeter(const AName: UString): TAction;
begin
Result := procedure; begin WriteLn('Olá, ', AName, '!'); end;
end function;
// Função de ordem superior: recebe a função a aplicar.
function ApplyTwice(AFunc: TTransform; AValue: Int32): Int32;
begin
Result := AFunc(AFunc(AValue));
end function;
var
LTick: TAction;
LPeek, LTriple: TTransform;
LGreet: TAction;
begin
LTick := MakeCounter(LPeek);
LTick();
LTick();
LTick();
WriteLn('contador depois de 3 ticks (+10): ', LPeek(10));
LTriple := MakeMultiplier(3);
WriteLn('triplo de 7: ', LTriple(7), '; duas vezes: ', ApplyTwice(LTriple, 7));
// função anônima passada direto, sem nome
WriteLn('quadrado aplicado duas vezes em 3: ',
ApplyTwice(function(const V: Int32): Int32; begin Result := V * V; end, 3));
LGreet := MakeGreeter('Ana');
LGreet();
end.contador depois de 3 ticks (+10): 13
triplo de 7: 21; duas vezes: 63
quadrado aplicado duas vezes em 3: 81
Olá, Ana!Demo 45. Contador compartilhado, fábrica de multiplicadores, captura de const de cadeia e função de ordem superior.
Como cada coisa é capturada#
| O que a closure usa | Como é capturado |
|---|---|
| variável local | vai para uma caixa compartilhada: a rotina e a closure veem a mesma variável, que vive enquanto alguma closure a segurar |
parâmetro por valor ou const | cópia, com contagem de referências: a closure guarda a sua referência e a solta ao morrer |
const de tipo gerenciado (cadeia, vetor, record com cadeia, classe, interface) | cópia segura; capturar vetor e record por const já funcionou errado e foi corrigido |
parâmetro var/out | só se a closure não puder sobreviver à rotina (veja abaixo) |
Uma closure que guarda objetos os mantém vivos (ARC). Um ciclo objeto ↔ closure precisa de weak.
Parâmetros var/out e noescape#
Copiar um var para dentro da closure seria uma cópia silenciosa que nunca chegaria ao chamador, então o compilador recusa o caso perigoso: a closure que pode sobreviver à rotina dona do parâmetro. Retornar a closure, por exemplo:
function MakeShow(var X: Int32): TRefProcedure;
begin
Result := procedure; begin WriteLn('X=', X); end; // FE1457
end function;O erro é FE1457: a closure só pode ser passada direto como argumento de uma rotina que apenas a chama (ou a repassa a parâmetros assim), ou cujo parâmetro seja declarado noescape; ou copie o parâmetro para uma variável local. Já a closure que não escapa funciona: atribuída a uma local e chamada dentro da própria rotina, ou passada a uma rotina que só a chama, ela altera o var de verdade (verificado: Inc(AX) dentro da closure muda a variável do chamador).
noescape é a promessa de uma rotina de não guardar nem repassar a closure recebida:
procedure CallIt(noescape AAction: TAction); // "const noescape" também vale
begin
AAction(); // só chamar, testar com Assigned ou repassar a outro noescape
end procedure;O compilador confere a promessa quando vê o corpo (FE1456 se o corpo a guarda) e confia nela quando não vê (método virtual, interface, módulo já compilado). FE1455: noescape só vale em parâmetro de valor ou const do tipo reference to.
Closure sem captura: objeto estático#
Uma função anônima que não captura nada (nem local, nem parâmetro, nem Self) e a conversão de uma rotina livre para reference to não alocam: todas as avaliações do mesmo ponto do fonte devolvem o mesmo objeto estático (ClosureStatic, ligado por padrão). É cerca de 5 vezes mais rápido e fica fora do relatório de vazamentos. A consequência visível é na identidade: duas avaliações da mesma lambda sem captura são iguais (Mk = Mk é True). Para o comportamento antigo: {$config ClosureStatic off}.
Veja também#
- Lambdas, eventos e async, Memória.
- Demo
45-closures; demo38-ponteiros-closures.