Mysterious Case Of Wrong Value
Anonymous methods in Delphi give us two things.
The first is the ability to write method code inline - passing it directly as a parameter to another method (function/procedure) or directly assigning it to a variable of the appropriate anonymous method type.
The second one is the ability to capture (use in method body) variables from the context in which a particular anonymous method is defined. This is especially useful for various callback and task related patterns because we can standardize (simplify) method signature and still have access to all necessary data from the outer context. Simply put, for every variable needed to perform particular functionality inside the method, we don't have to introduce another parameter.
Neat!
You decide to put together a simple piece of code to explore new possibilities.
What the heck? What happened here? Is this a bug?
No it's not a bug, it's a feature.
Delphi anonymous methods capture the locations of variables, not their values at specific point during code execution.
Anonymous methods are basically defined as interfaces with a single method -
In the above example, by the time the declared anonymous functions are called, the
If we call the function inside the
However, calling the function during the loop defeats the purpose of creating an anonymous function in the first place. Also, if you use anonymous methods to execute some parallel tasks, you cannot count on the captured variables to have the expected values at the moment of task execution.
To solve that problem, you have to capture the actual value of the loop variable
Wasn't the whole point of this exercise simplifying the method signature and now we ended up with an additional function? How is that simpler?
Well, the point was in simplifying anonymous method signature so we don't have to deal with different anonymous method types across some complex framework. For instance, the Delphi Parallel Programming Library (PPL) can use two types of methods when creating tasks. One is
Even though sometimes anonymous methods and their variable capture mechanism need a bit more code than we would like to write, they are still an extremely powerful and huge productivity feature.
The first is the ability to write method code inline - passing it directly as a parameter to another method (function/procedure) or directly assigning it to a variable of the appropriate anonymous method type.
The second one is the ability to capture (use in method body) variables from the context in which a particular anonymous method is defined. This is especially useful for various callback and task related patterns because we can standardize (simplify) method signature and still have access to all necessary data from the outer context. Simply put, for every variable needed to perform particular functionality inside the method, we don't have to introduce another parameter.
Neat!
You decide to put together a simple piece of code to explore new possibilities.
uses
System.SysUtils;
procedure Test;
var
Functions: array of TFunc<Integer>;
Func: TFunc<Integer>;
i: Integer;
begin
SetLength(Functions, 5);
for i := 0 to High(Functions) do
Functions[i] :=
function: Integer
begin
Result := i;
end;
for Func in Functions do
Writeln(Func());
end;
begin
Test;
end.
You happily run the above code expecting integers from 0 to 4 as output.
5
5
5
5
5
What the heck? What happened here? Is this a bug?
No it's not a bug, it's a feature.
Delphi anonymous methods capture the locations of variables, not their values at specific point during code execution.
Anonymous methods are basically defined as interfaces with a single method -
Invoke
-
implemented by a hidden reference counted class, and captured variables are stored as fields
of that class. When an anonymous method is accessed, an instance of that class is constructed
behind the scenes, and it is kept alive through reference counting for as long as is required
by the anonymous method it wraps.In the above example, by the time the declared anonymous functions are called, the
for
loop where the functions
are defined is finished and its loop variable i
contains the value 5, and that is the value our
anonymous functions will return when called.If we call the function inside the
for
loop, it will return the current value of the for
loop variable
i
and output the numbers 0 to 4.However, calling the function during the loop defeats the purpose of creating an anonymous function in the first place. Also, if you use anonymous methods to execute some parallel tasks, you cannot count on the captured variables to have the expected values at the moment of task execution.
To solve that problem, you have to capture the actual value of the loop variable
i
during the
loop. Wrapping the anonymous function declaration into a regular function and passing all
necessary variables as parameters (in this example only one) will create copies of their
values on the stack and allow the anonymous method capture mechanism to capture each particular
copy (and its value) instead of the original.
uses
System.SysUtils;
function CreateFunction(Value: Integer): TFunc<Integer>;
begin
Result :=
function: Integer
begin
Result := Value;
end;
end;
procedure Test;
var
Functions: array of TFunc<Integer>;
Func: TFunc<Integer>;
i: Integer;
begin
SetLength(Functions, 5);
for i := 0 to High(Functions) do
Functions[i] := CreateFunction(i);
for Func in Functions do
Writeln(Func());
end;
begin
Test;
end.
0
1
2
3
4
Wasn't the whole point of this exercise simplifying the method signature and now we ended up with an additional function? How is that simpler?
Well, the point was in simplifying anonymous method signature so we don't have to deal with different anonymous method types across some complex framework. For instance, the Delphi Parallel Programming Library (PPL) can use two types of methods when creating tasks. One is
TNotifyEvent
and other is TProc
- a parameterless anonymous method. Without anonymous
methods and their capture mechanism, creating different tasks that require additional
parameters would have to be implemented inside PPL. That would transform clean library code
into a huge mess, and every now and then it would not be enough to solve a particular problem.Even though sometimes anonymous methods and their variable capture mechanism need a bit more code than we would like to write, they are still an extremely powerful and huge productivity feature.
Comments
Post a Comment