[教程]Delphi 中的匿名函数详解

Delphi 支持匿名函数已经很久了,在以前的文章中,我说过,Delphi 的匿名函数实际上是一个接口,当然这个接口是没有接口的GUID的。那么 Delphi 是如何动态生成这个接口类型的呢?我们以一个实际的例子和接口在内存中的布局来详细说明下:

1、首先,接口三大金刚(AddRef/Release/QueryInterface)咱们忽略掉不谈,既然是接口,它们肯定存在,我们可以略过去。我们先用下面的代码来确认一下这一事实:

procedure TForm2.Button1Click(Sender: TObject);
var
  ACallback: TThreadProcedure;
  AObj: TObject;
const
  ObjCastGUID: TGUID = '{CEDF24DE-80A4-447D-8C75-EB871DC121FD}';
begin
  ACallback := procedure
    begin
      Caption:='Hello,world';
    end;
  if Supports(IInterface(PPointer(@ACallback)^), ObjCastGUID, AObj) then
    ShowMessage(AObj.ClassName);
  TThread.Queue(nil, ACallback);
end;

我们知道,Delphi 中 TObject 是所有类的基类,所以我们尝试将定义的匿名函数实例,转换为 TObject,然后通过 ClassName 来取出类型名称。执行上面的代码,出现下面的结果:

Delphi中匿名函数类型

出现了它的真正类型,叫TForm2.Button1Click$ActRec,好的,完美证明了我们的结论(上面的ObjCastGUID 取自 System.pas)。

2、接下来,我们写一个函数,看看其 RTTI 信息有没有,如果有,将其RTTI信息输出,这里分解下步骤:

2.1、写一个函数,来输出类的继承关系

function DumpClassInherits(AClassType: TClass): String;
  var
    AParentClass: TClass;
  begin
    AParentClass := AClassType.ClassParent;
    if Assigned(AParentClass) then
      Result := DumpClassInherits(AClassType.ClassParent) + '->' + AClassType.ClassName
    else
      Result := AClassType.ClassName;
  end;

将第一步中,AObj.ClassType 作为参数传进去后,我们可以得到下面的结果:

匿名函数接口的继承关系

TObject->TInterfacedObject->TForm2.Button1Click$ActRec

进一步验证了我们以前的结论,别看二鬼子穿身皮,扒下来会发现它真的就是一个接口。

2.2、再写一个函数打印一下其函数成员和数据成员的信息

procedure TForm2.DumpRtti(AObj: TObject);
var
  AContext: TRttiContext;
  AType: TRttiType;
  AFields: TArray<TRttiField>;
  AMethods: TArray<TRttiMethod>;
  AProps: TArray<TRttiProperty>;
  I: Integer;
begin
  AContext := TRttiContext.Create;
  AType := AContext.GetType(AObj.ClassType.ClassInfo);
  if Assigned(AType) then
  begin
    AMethods := AType.GetMethods;
    Memo1.Lines.Add('Methods:');
    for I := 0 to High(AMethods) do
      Memo1.Lines.Add(' '+IntToStr(I + 1) + '.' + AMethods[I].Name);
    AFields := AType.GetFields;
    Memo1.Lines.Add('Fields:');
    for I := 0 to High(AFields) do
      Memo1.Lines.Add(' '+IntToStr(I + 1) + '.' + AFields[I].Name);
    AProps := AType.GetProperties;
    Memo1.Lines.Add('Properties:');
    for I := 0 to High(AProps) do
      Memo1.Lines.Add(' '+IntToStr(I + 1) + '.' + AProps[I].Name);
  end;
end;

实测输出内容如下:

匿名函数 RTTI 输出信息

Methods:
1.AfterConstruction
2.BeforeDestruction
3.NewInstance
4.Create
5.Free
6.DisposeOf
7.InitInstance
8.CleanupInstance
9.ClassType
10.ClassName
11.ClassNameIs
12.ClassParent
13.ClassInfo
14.InstanceSize
15.InheritsFrom
16.MethodAddress
17.MethodAddress
18.MethodName
19.QualifiedClassName
20.FieldAddress
21.FieldAddress
22.GetInterface
23.GetInterfaceEntry
24.GetInterfaceTable
25.UnitName
26.UnitScope
27.Equals
28.GetHashCode
29.ToString
30.SafeCallException
31.AfterConstruction
32.BeforeDestruction
33.Dispatch
34.DefaultHandler
35.NewInstance
36.FreeInstance
37.Destroy
Fields:
1.Self
2.FRefCount
Properties:
1.RefCount

好的,我们看到了什么?Self!!!!

所以接下来的事情我们可以继续玩了。

3、在上一步的实测中,我们发现了一个重要的东西,Self,这意味着我们可以除了匿名函数体内,得到这个匿名函数所关联的对象的实例!所以我们来根据这些信息,来做进一步的验证:

3.1、首先,如果匿名函数中,没有引用对象实例的代码,会不会还有 Self 指针?很简单,我们将前面的代码中

Caption:='Hello,world';

改成:

OutputDebugString('Hello,world');

然后看一下输出 RTTI 信息,为节省篇幅,咱们只看 Fields 部分(下同):

解除引用后匿名函数接口成员

……
Fields:
1.FRefCount
Properties:
……

只有一个引用计数成员了。由此推论:

只有匿名函数中引用的内容,才会被加入到自动生成的接口中。

3.2、第二个问题:匿名函数中的成员加入的顺序是什么样子?为此,我们将上面的代码进一步改动:

procedure TForm2.Button1Click(Sender: TObject);
var
  ACallback: TThreadProcedure;
  AObj: TObject;
  B: Byte;
  I: Integer;
const
  ObjCastGUID: TGUID = '{CEDF24DE-80A4-447D-8C75-EB871DC121FD}';
begin
  ACallback := procedure
    var
      R:Integer;
    begin
      R:=I+B;
      Caption :=R.ToString;
    end;
  if Supports(IInterface(PPointer(@ACallback)^), ObjCastGUID, AObj) then
    DumpRtti(AObj);
  TThread.Queue(nil, ACallback);
end;

我们仍来看一下 RTTI 信息的输出:

匿名函数成员排列顺序测试结果

Fields:
1.Self
2.B
3.I

接下来我们调整下匿名函数中代码的顺序,然后再看一下:

ACallback := procedure
    var
      R:Integer;
    begin
      Caption :=I.ToString;
      R:=B+I;
      Caption:=R.ToString;
    end;

我们来看下输出:

匿名函数成员排列顺序测试结果

Fields:
1.B
2.I
3.Self

由此可以得出今天的第二个结论:

匿名函数接口定义中,成员的顺序排列的取决于函数内容引用的顺序,后引用的在前面。

同时,根据上面的实践,我们还可以引申出一个结论:

被匿名函数引用的局部变量的分配,是被放到匿名对应的接口上的,而不是在当前线程栈上。因此,它们占用的内存,是在这个匿名函数接口被释放时释放的。

关于上面的这一点,大家可以用如下的代码来简单验证:

var
  ABeforeList:Integer;
  AList:TList;
  AfterList:Integer;
begin
  AList:=TList.Create;
  ABeforeList:=0;
  AfterList:=1;
  Memo1.Lines.Add('BeforeList='+IntToHex(IntPtr(@ABeforeList),SizeOf(Pointer)));
  Memo1.Lines.Add('List='+IntToHex(IntPtr(@AList),SizeOf(Pointer)));
  Memo1.Lines.Add('AfterList='+IntToHex(IntPtr(@AfterList),SizeOf(Pointer)));
  TThread.ForceQueue(nil,
    procedure
    begin
      Caption:=AList.Count.ToString;
      FreeAndNil(AList);
    end
    );
end;

上面的代码输出是这样的:

BeforeList=BBFC24
List=37130EC
AfterList=BBFC20

注意加粗的这行,其内存地址与其它两个元素完全不搭界,直接证明了我们提出的观点。

好吧,这么长的文章如果你能一次性看完,说明在观看俄罗斯和乌克兰战争的同时,你还在对技术感兴趣,这样的人才是真正的码农。所以额外造成你一个残酷的事实:

在同一个函数体中定义的不同回调函数,其类型是也是同一个名称。所以如果你要靠名称判断两个类型是否一致的话,可以跳楼了。

好了,本文到此为止,有兴趣的同学可以继续深入研究下。

分享到: