Articles Mysterious Case Of Wrong Value (Is it a bug or developer-bugged?) by Dalija Prasnikar

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
Mysterious Case Of Wrong Value
Dalija Prasnikar - 06/Aug/2018
[SHOWTOGROUPS=4,20]
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.
Код:
unit Unit1;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  System.SysUtils,
  System.Variants,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure Test;
var
  Functions: array of TFunc<Integer>;
  Func     : TFunc<Integer>;
  i        : Integer;
begin
  SetLength(Functions, 5);
  //
  for i := 0 to High(Functions) do
  begin
    Functions[i] := function: Integer
      begin
        Result := i;
      end;

  end;
  //
  for Func in Functions do
    Form1.Memo1.Lines.Add(Func().ToString);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  //
  Test;
end;

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.

Код:
unit Unit1;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  System.SysUtils,
  System.Variants,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure Test; // possible error or bug on resulted!!!
var
  Functions: array of TFunc<Integer>;
  Func     : TFunc<Integer>;
  i        : Integer;
begin
  SetLength(Functions, 5);
  //
  for i := 0 to High(Functions) do
  begin
    Functions[i] := function: Integer
      begin
        Result := i;
      end;

  end;
  //
  for Func in Functions do
    Form1.Memo1.Lines.Add(Func().ToString);
end;

function CreateFunction(Value: Integer): TFunc<Integer>;
begin
  Result := function: Integer
    begin
      Result := Value;
    end;
end;

procedure Test2; // try solve it!!!
var
  Functions: array of TFunc<Integer>;
  Func     : TFunc<Integer>;
  i        : Integer;
begin
  SetLength(Functions, 5);
  //
  for i := 0 to High(Functions) do
  begin
    Functions[i] := CreateFunction(i);
  end;
  //
  for Func in Functions do
    Form1.Memo1.Lines.Add(Func().ToString);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  //
  Test;
  //
  Form1.Memo1.Lines.Add('');
  //
  Test2;
end;

end.

new resulted:

Код:
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.

[/SHOWTOGROUPS]