问题 如何比较包含对象的函数/过程的TFunc / TProc?


我们用一个 TList<TFunc<Boolean>> 和一些 function ... of object在它,现在想 Remove() 一些条目再次。但它不起作用,因为显然你很简单 不能 比较这些 reference to ... 可靠的东西。

这是一些测试代码:

program Project1;

{$APPTYPE CONSOLE}

uses
  Generics.Defaults,
  SysUtils;

type
  TFoo = class
  strict private
    FValue: Boolean;
  public
    constructor Create();
    function Bar(): Boolean;
  end;

{ TFoo }

function TFoo.Bar: Boolean;
begin
  Result := FValue;
end;

constructor TFoo.Create;
begin
  inherited;

  FValue := Boolean(Random(1));
end;

function IsEqual(i1, i2: TFunc<Boolean>): Boolean;
begin
  Result := TEqualityComparer<TFunc<Boolean>>.Default().Equals(i1, i2);
end;

var
  s: string;
  foo: TFoo;
  Fkt1, Fkt2: TFunc<Boolean>;

begin
  try
    Foo := TFoo.Create();

    WriteLn(IsEqual(Foo.Bar, Foo.Bar));             // FALSE (1)
    WriteLn(IsEqual(Foo.Bar, TFoo.Create().Bar));   // FALSE (2)

    Fkt1 := function(): Boolean begin Result := False; end;
    Fkt2 := Fkt1;
    WriteLn(IsEqual(Fkt1, Fkt2));                   // TRUE  (3)

    Fkt2 := function(): Boolean begin Result := False; end;
    WriteLn(IsEqual(Fkt1, Fkt2));                   // FALSE (4)

    Fkt2 := function(): Boolean begin Result := True; end;
    WriteLn(IsEqual(Fkt1, Fkt2));                   // FALSE (5)

    FreeAndNil(Foo);
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
  Readln(s);
end.

我们虚拟地尝试过 一切,=运算符,比较指针等。

我们甚至尝试了一些非常讨厌的事情,比如反复投射 PPointer 并取消引用,直到我们得到相同的值,但这当然不会产生令人满意的结果=)。

  • 情况(2),(4)和(5)都可以,因为实际上存在不同的功能。
  • 案例(3)也是微不足道的。
  • 案例(1)是我们想要检测的,这是我们无法开展的工作。

我担心,Delphi会隐秘地创建两个不同的匿名函数来转发调用 Foo.Bar。在这种情况下,我们将完全无能为力,除非我们想要躲过未知记忆的泥沼......而且,我们不会。


12890
2018-03-01 10:53


起源

+1,因为这些匿名引用是weired。它们里面有什么?我已经做了 var F: TFunc<Boolean>; ShowMessage(IntToStr(SizeOf(F)));  - 它为我的Delphi 2010显示1!怎么可能? - Cosmin Prund
你指的是那些案件是什么? - Martijn
@Cosmin - 它返回表达式F的类型的大小,在你的情况下是布尔值,因为F是一个返回布尔值的函数。 - Barry Kelly
@Martijn:我指的是评论中的数字。 @Cosmin:看起来它们长4个字节,至少是里面参数的地址 IsEqual 相距4个字节(可能是对齐)。 - kiw
@kiw:啊,好的。没见过那些,对不起。 - Martijn


答案:


您必须通过其他方式将名称或索引与它们相关联。匿名方法没有名称,可能捕获状态(因此每个实例重新创建它们);没有破坏封装,没有任何简单的方法可以使它们具有可比性。

你可以得到方法引用背后的对象,如果它背后确实有一个对象(不能保证这一点 - 方法引用的接口是根据COM语义实现的,它们真正需要的只是一个COM vtable):

function Intf2Obj(x: IInterface): TObject;
type
  TStub = array[0..3] of Byte;
const
  // ADD [ESP+$04], imm8; [ESP+$04] in stdcall is Self argument, after return address
  add_esp_04_imm8: TStub = ($83, $44, $24, $04);
  // ADD [ESP+$04], imm32
  add_esp_04_imm32: TStub = ($81, $44, $24, $04);

  function Match(L, R: PByte): Boolean;
  var
    i: Integer;
  begin
    for i := 0 to SizeOf(TStub) - 1 do
      if L[i] <> R[i] then
        Exit(False);
    Result := True;
  end;

var
  p: PByte;
begin
  p := PPointer(x)^; // get to vtable
  p := PPointer(p)^; // load QueryInterface stub address from vtable

  if Match(p, @add_esp_04_imm8) then 
  begin
    Inc(p, SizeOf(TStub));
    Result := TObject(PByte(Pointer(x)) + PShortint(p)^);
  end
  else if Match(p, @add_esp_04_imm32) then
  begin
    Inc(p, SizeOf(TStub));
    Result := TObject(PByte(Pointer(x)) + PLongint(p)^);
  end
  else
    raise Exception.Create('Not a Delphi interface implementation?');
end;

type
  TAction = reference to procedure;

procedure Go;
var
  a: TAction;
  i: IInterface;
  o: TObject;
begin
  a := procedure
    begin
      Writeln('Hey.');
    end;
  i := PUnknown(@a)^;
  o := i as TObject; // Requires Delphi 2010
  o := Intf2Obj(i); // Workaround for non-D2010
  Writeln(o.ClassName);
end;

begin
  Go;
end.

这将(目前)打印 Go$0$ActRec;但是如果你有第二个匿名方法,结构相同,它将导致第二个方法,因为匿名方法体不进行结构相等性比较(这将是一个高成本,低价值的优化,因为程序员不太可能做这样的事情,大型结构比较并不便宜)。

如果您使用的是更高版本的Delphi,则可以在此对象的类上使用RTTI并尝试比较字段,并自行实现结构比较。


14
2018-03-01 11:33



@Barry什么时候捕获的状态?假设我有 f1, f2: TProc。然后我定义 f1 它捕获了状态。稍后在相同的例程中,如果我分配 f2 := f1,是再次被捕的国家? - David Heffernan
我没有博客,我无法将这么多代码放在评论中,所以这里是变量被捕获的时间和方式: stackoverflow.com/questions/5154914/... - Cosmin Prund
@David在我写的示例代码中没有捕获任何状态;但一般来说,匿名方法对状态(变量和参数)的访问在编译时(对堆分配对象的字段)进行重写,并且在进程入口时分配此状态。取消分配时取决于最后一个方法引用何时超出范围(即引用计数)。 - Barry Kelly
@kiw - TObject转换的东西,我不记得它发布的版本 - 我想它可能是Delphi 2010,对不起。但同样,如果你没有RTTI来比较字段,那对你来说可能没什么大帮助。还有另一种从接口到对象实例的方法 - 切换到CPU视图并逐步执行接口或方法引用调用的操作码,您将看到它通过一个存根,在更改EAX后跳转到最终目标(在注册调用约定) - 对EAX的这种改变是接口引用和对象类型Self之间的增量。 - Barry Kelly
@kiw - 没问题;只是要知道你已经使用了我的一些hacky代码,虽然经过专业测试......;) - Barry Kelly