【问题标题】:How are anonymous methods implemented under the hood?匿名方法是如何在后台实现的?
【发布时间】:2017-02-18 16:26:13
【问题描述】:

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

由于匿名方法还捕获外部变量并延长它们的生命周期,因此了解这些变量何时会从内存中“释放”非常重要。

在另一个匿名方法中声明一个匿名方法可能有哪些缺点。 可以循环引用吗?

【问题讨论】:

标签: delphi anonymous-methods


【解决方案1】:

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

X = interface
  function Invoke(a: Integer): string;
end;

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

当你内联声明一个匿名方法时,编译器会在例程的序言中创建一些代码,以确保捕获的任何变量都不存在于堆栈上而是存在于堆上(这也是你无法检查的原因调试期间捕获的任何变量,因为不幸的是它缺少该信息)。编译器在该接口后面创建一个类,其字段与您正在捕获的变量同名(有关更多信息,请参阅this blog article)。

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

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

【讨论】:

  • 我以为捕获的变量(局部变量、参数、全局变量)被实现为接口的字段(成员)?我知道这是计划,但如果真的是这样,我从来没有仔细观察过。
  • @RudyVelthuis 接口什么时候可以有字段?
  • 实现对象是匿名方法实现。该接口仅用于生命周期管理。正如 Barry Kelly(实现 anonmeths)所说:“基本上,变量 'x'(或任何其他被匿名方法触及的变量)被吊出并变成类上的一个字段。该类的一个实例是在输入函数时创建,匿名方法被转换为隐藏类的方法。”在这里查看他的 cmets:reddit.com/r/programming/comments/6tp3i/…
  • @RudyVelthuis 这是我在第三段中写的。
  • @StefanGlienke 在考虑您在最后一段中概述的问题时,我意识到只需一个引用自身的匿名方法即可产生该问题。我的回答现在说明了这一点。
【解决方案2】:

匿名方法被实现为接口。这篇文章很好地解释了编译器是如何完成的:Anonymous methods in Delphi: the internals

本质上,编译器生成的接口只有一个名为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.

编译器在幕后创建了一个实现匿名方法接口的隐藏类。该类包含作为数据成员捕获的任何变量。当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.

可以通过明确清除自引用来打破循环。在看起来像这样的匿名方法示例中:

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

接口变体的等价物是:

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

【讨论】:

  • 方法 2 似乎没有在这里捕获方法 1
  • @Arioch'The 不需要循环和内存泄漏。
  • 是的,我看到了 Stefan 回答的最后一段,但您的回答没有提及/解释它。当线性参考图突然变成圆形时,您的答案没有解释
  • @Arioch'The 在我的编辑中,我制作了一个更简单的示例,并对其进行了解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-16
  • 2012-09-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多