Anonymous methods - variable capture versus value capture
Below is a SSCCE based on an example in the Anonymous Methods section of Part 1 of Chris Rolliston's excellent Delphi XE2 Foundations book, about the idea of variable capture (any errors in it are entirely down to me).
It works exactly as I'd expect, logging 666, 667, 668, 669 on successive clicks of the BtnInvoke button. In particular it nicely illustrates how the captured version of the local variable I persists long after btnSetUpClick exits.
So far so good. The problem I'm asking about isn't with this code per se but with what's said in Allen Bauer's blog here:
http://blogs.embarcadero.com/abauer/2008/10/15/38876
Now, I know better than to argue with the boss, so I am sure I'm missing the point of the distinction he draws between variable capture and value capture. To my simple way of looking at it, my CR-based example captures the value of I by capturing I as a variable.
So, my question is, what is the distinction Mr Bauer is trying to draw, exactly?
(Btw, despite watching the Delphi section of SO daily for 9+ months, I'm still not entirely clear if this q is on-topic. If not, my apologies and I'll take it down.)
type
TAnonProc = reference to procedure;
var
P1,
P2 : TAnonProc;
procedure TForm2.Log(Msg : String);
begin
Memo1.Lines.Add(Msg);
end;
procedure TForm2.btnSetUpClick(Sender: TObject);
var
I : Integer;
begin
I := 41;
P1 := procedure
begin
Inc(I);
Log(IntToStr(I));
end;
I := 665;
P2 := procedure
begin
Inc(I);
Log(IntToStr(I));
end;
end;
procedure TForm2.btnInvokeClick(Sender: TObject);
begin
Assert(Assigned(P1));
Assert(Assigned(P2));
P1;
P2;
end;
Variable capture vs value capture is simple enough. Let us suppose that two anonymous methods capture the same variable. Like this:
Type
TMyProc = reference to procedure;
var
i: Integer;
P1, P2: TMyProc;
....
i := 0;
P1 := procedure begin Writeln(i); inc(i); end;
P2 := procedure begin Writeln(i); inc(i); end;
P1();
P2();
Writeln(i);
There is a single variable that is captured by both methods. The output is:
0 1 2
This is capture of a variable. If the value was captured, which it isn't, one might imagine that the two methods would have separate variables that both started with value 0. And both functions might output 0.
In your example, you are supposed to imagine that P1
captures the value 41
, and P2
captures the value 665
. But that does not happen. There is exactly one variable. It is shared between the procedure that declares it, and the anonymous methods that capture it. It lives as long as all parties that share it live. Modifications to the variable made by one party are seen by all others because there is exactly one variable.
So, it is not possible to capture a value. To get behaviour that feels like that you need to copy a value to a new variable, and capture that new variable. That can be done with, for instance, a parameter.
function CaptureCopy(Value: Integer): TMyProc;
begin
Result := procedure begin Writeln(Value); end;
end;
...
P3 := CaptureCopy(i);
This will copy the value of i
into a new variable, the procedure parameter, and capture that. Subsequent changes to i
have no influence on P3
because the captured variable is local to P3
.
Let us clarify the things a bit; internally any data captured by an anonymous method is a field of a hidden object instance and should be called variable as such; but there can be different cases of capturing a variable.
Consider the sample code:
type
TMyProc = reference to procedure;
function CaptureValue(Value: Integer): TMyProc;
begin
Result := procedure begin Inc(Value); Writeln(Value); end;
end;
procedure Test1;
var
Proc1: TMyProc;
I: Integer;
begin
I:= 32;
Proc1:= CaptureValue(I);
Proc1();
Writeln(I); // 32
end;
procedure Test2;
var
Proc2: TMyProc;
I: Integer;
begin
I:= 32;
Proc2:= procedure begin Inc(I); Writeln(I); end;
Proc2();
Writeln(I); // 33
end;
You can see that logically the Proc1
(in Test1
) captures a value of I
while the Proc2
(in Test2
) captures a reference to I
.
If you look under the hood you'll notice that the variable I
in Test1
is a plain local stack-based variable while Proc1
accesses a field of a hidden object instance (using reference to the instance and offset of the field); we have 2 different variables (one on stack and the other on heap).
Test2
has no stack-based I
variable at all, only a field of a hidden object instance; both Test2
and Proc2
access the same variable by reference to the instance (and the field's offset); we have a single heap variable.