Yesterday I wrote a post about executing some code automatically when a method ends. Today I’ll show you why this approach must be used with care.
Let’s start with a working example. Create Delphi VCL application, throw a TListBox and a TButton on the form and write this simple Button1.OnClick event. [Pardon my ugly formatting; I was trying to save some space.]
procedure TForm8.Button1Click(Sender: TObject);
begin
AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute 1');
end);
AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute 2');
end);
ListBox1.Items.Add('Will end now …');
end;
The output from this program will be partially expected and (maybe) partially surprising.
As we see the code is indeed executed at the end of the Button1Click method, but interfaces are destroyed in the reverse order of creation (which usually doesn’t cause any problems).
The code created by the compiler is equivalent to this program.
procedure TForm8.Button1Click(Sender: TObject);
var
temp1, temp2: IGpAutoExecute;
begin
temp1 := nil; temp2 := nil;
try
temp1 := AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute 1');
end);
temp2 := AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute 2');
end);
ListBox1.Items.Add('Will end now …');
finally
temp2 := nil;
temp1 := nil;
end;
end;
Next, I’ll show an example of a bad pattern usage.
procedure TForm8.Button1Click(Sender: TObject);
var
i: integer;
begin
for i := 1 to 5 do
AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute');
end);
ListBox1.Items.Add('Will end now …');
end;
The output may surprise you.
Only one AutoExecute code segment is executed at the end of the method! Why?
Let’s see what the compiler really does in this case.
procedure TForm8.Button1Click(Sender: TObject);
var
i: integer;
temp: IGpAutoExecute;
begin
temp := nil;
try
for i := 1 to 5 do
temp := AutoExecute(procedure begin
ListBox1.Items.Add('AutoExecute');
end);
ListBox1.Items.Add('Will end now …');
finally
temp := nil;
end;
end;
Compiler only creates one temporary variable and it is reused inside the for statement. Every additional assignment clears the previous content and causes destructor to be called. Only the last instance (created when i = 5) is destroyed when method exits.
There’s additional problem with running anonymous methods from a for loop. Because the values are captured by a reference, not by a value (IOW, when you call AutoExecute in the code above, it gets an address of the for loop variable i, not its content), the value when destructor is called does not equal the value at the moment when AutoExecute is called.
procedure TForm8.Button1Click(Sender: TObject);
var
i: integer;
begin
for i := 1 to 5 do
AutoExecute(
procedure
begin
ListBox1.Items.Add('AutoExecute #' + IntToStr(i));
end);
ListBox1.Items.Add('Will end now …');
end;
If you need to create anonymous methods inside a loop, use the following trick.
function TForm8.AutoLog(value: integer): TProc;
begin
Result := procedure begin
ListBox1.Items.Add('AutoExecute #' + IntToStr(value));
end;
end;
procedure TForm8.Button1Click(Sender: TObject);
var
i: integer;
begin
for i := 1 to 5 do
AutoExecute(AutoLog(i));
ListBox1.Items.Add('Will end now …');
end;
By moving anonymous method creation into a separate function we are ensuring that correct value of the parameter is captured. Of course, the code is still called at the wrong time but at least the values are correct …
Heh this reminds me why using
ReplyDeleteprocedure TMyThread.Execute;
....
(myIntf as IMyInterface).DoSoemthing;
....
while not Terminated do
....
end;
is bad. It took me a while to figure why my classes was leaking.
I don't get it, if you declare a method, why use anonymous method?
ReplyDeleteI don't understand your question.
DeleteThis remember-me that you need to know when your destructors run. https://blogs.msdn.com/b/oldnewthing/archive/2004/05/20/135841.aspx?Redirected=true
ReplyDeleteMust admit that I'm not a fan of this kind of code...