如何在引擎盖下实施匿名方法?

lok*_*oki 10 delphi anonymous-methods

Delphi是否"实例化"每个匿名方法(如对象)?如果是这样,Delphi何时创建此实例,最重要的是,Delphi何时释放它?

因为匿名方法还捕获外部变量并延长其生命周期,所以知道这些变量何时将从内存中"释放"非常重要.

在另一个匿名方法中声明匿名方法有什么可能的缺点.是否可以循环引用?

Dav*_*nan 15

匿名方法作为接口实现.本文很好地解释了编译器是如何完成的:Delphi中的匿名方法:内部.

本质上,编译器生成的接口有一个名为的方法Invoke,其后面是您提供的匿名方法.

捕获的变量与捕获它们的任何匿名方法具有相同的生命周期.匿名方法是一个接口,其生命周期由引用计数管理.因此,捕获的变量生命延长与捕获它们的匿名方法一样长.

正如可以使用接口创建循环引用一样,同样可以使用匿名方法创建循环引用.这是我可以构建的最简单的演示:

uses
  System.SysUtils;

procedure Main;
var
  proc: TProc;
begin
  proc :=
    procedure
    begin
      if Assigned(proc) then
        Beep;
    end;
end;

begin
  ReportMemoryLeaksOnShutdown := True;
  Main;
end.
Run Code Online (Sandbox Code Playgroud)

在幕后,编译器创建了一个实现匿名方法接口的隐藏类.该类包含任何捕获的变量作为数据成员.当proc被分配到,增加对执行实例的引用计数.由于它proc是由实现实例拥有的,因此该实例已经引用了它自己.

为了使这个更清楚一点,这个程序提出了相同的问题,但是在接口方面重新构建:

uses
  System.SysUtils;

type
  ISetValue = interface
    procedure SetValue(const Value: IInterface);
  end;

  TMyClass = class(TInterfacedObject, ISetValue)
    FValue: IInterface;
    procedure SetValue(const Value: IInterface);
  end;

procedure TMyClass.SetValue(const Value: IInterface);
begin
  FValue := Value;
end;

procedure Main;
var
  intf: ISetValue;
begin
  intf := TMyClass.Create;
  intf.SetValue(intf);
end;

begin
  ReportMemoryLeaksOnShutdown := True;
  Main;
end.
Run Code Online (Sandbox Code Playgroud)

通过明确清除自引用可以打破循环.在匿名方法示例中,如下所示:

procedure Main;
var
  proc: TProc;
begin
  proc :=
    procedure
    begin
      if Assigned(proc) then
        Beep;
    end;
  proc := nil;
end;
Run Code Online (Sandbox Code Playgroud)

接口变体的等价物是:

procedure Main;
var
  intf: ISetValue;
begin
  intf := TMyClass.Create;
  intf.SetValue(intf);
  intf.SetValue(nil);
end;
Run Code Online (Sandbox Code Playgroud)


Ste*_*nke 8

匿名方法实现为具有名为Invoke的方法的接口,该方法具有与匿名方法声明相同的签名.所以从技术上讲,这reference to function(a: Integer): string是一个二进制兼容的接口:

X = interface
  function Invoke(a: Integer): string;
end;
Run Code Online (Sandbox Code Playgroud)

在几个版本之前,甚至可以在匿名方法上调用.Invoke,但编译器现在可以阻止它.

当您声明内联的匿名方法时,编译会在例程的序言中创建一些代码,以确保捕获的任何变量不会在堆栈上但在堆上(这也是您无法检查任何捕获的变量的原因)在调试期间,因为它很遗憾缺少这些信息).编译器在该接口后面创建一个类,其中的字段与您捕获的变量具有相同的名称(有关更多信息,请参阅此博客文章).

至于循环引用,是的.请注意,当您例如捕获一个接口(或者对于启用了对象的ARC的nextgen平台的对象)时,可能会导致循环引用导致内存泄漏.

另外有趣的是,如果在同一例程中有多个匿名方法,则它们都由相同的编译器生成对象支持.这可能会导致出现内存泄漏的另一种情况,因为一个匿名方法也可能捕获另一个并创建另一个循环引用.

  • @RudyVelthuis从什么时候开始接口都有字段? (2认同)
  • @RudyVelthuis这是我在第三段写的. (2认同)