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.
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

Popular posts from this blog

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code

Beware of loops and tasks