This was prompted by How to compare TFunc/TProc containing function/procedure of object?, specifically by David's comment to Barry's question. Since I don't have a Blog to post this to I'm going to ask this question here, and answer it.
Question: When and how are variables referenced in Delphi's anonymous methods captured?
Example:
procedure ProcedureThatUsesAnonymousMethods;
var V: string;
F1: TFunc<string>;
F2: TFunc<string>;
begin
F1 := function: string
begin
Result := V; // references local variable
end
V := '1';
F2 := function: string
begin
Result := V;
end
V := '2';
ShowMessage(F1);
ShowMessage(F2);
end;
Both ShowMessage
are going to show 2
. Why? How does V
get captured and when?
When you have a function like the one in the question, where you have an anonymous method accessing a local variable, Delphi appears to create one TInterfacedObject descendant that captures all the stack based variables as it's own public variables. Using Barry's trick to get to the implementing TObject and a bit of RTTI we can see this whole thing in action.
The magic code behind the implementation probably looks like this:
Of course this code doesn't compile. I'm magic-less :-) But the idea here is that an "Magic" object is created behind the scenes and local variables that are referenced from the anonymous method are transformed in public fields of the magic object. That object is uses as an interface (IUnkown) so it gets reference-counted. Apparently the same object captures all used variables AND defines all the anonymous methods.
This should answer both "When" and "How".
Here's the code I used to investigate. Put a TButton on a blank form, this should be the whole unit. When you press the button you'll see the following on screen, in sequence:
TForm25.Button1Click$ActRec: TInterfacedObject
: This shows the object behind the implementation, it's derived from TInterfacedObjectOnStack:string
: RTTI discovers this field on that object.Self: TForm25
: RTTI discovers this field on that object. It's used to get the value ofClasVar
FRefCount:Integer
- this comes from TInterfacedObjectClass Var
- result of ShowMessage.On Stack
- result of ShowMessage.Here's the code: