比较 2 个接口 (IControl) 的好方法是什么?这是 Delphi 中的错误吗?

lok*_*oki 8 delphi firemonkey

在Delphi的源代码中,我在FMX.Forms单元中看到了这样的内容:

procedure TCommonCustomForm.SetHovered(const Value: IControl);
begin
  if (Value <> FHovered) then
  begin
    ....
  end;
end;
Run Code Online (Sandbox Code Playgroud)

我认为这样做Value <> FHovered 从根本上来说是错误的,因为Value <> FHovered可以返回 true 并且同时两者都Value可以FHovered指向同一个TControl对象。我错了吗?(注意这是我在调试中看到的)。

现在有一个附属问题:为什么两个IControl接口可以不同(从指针的角度来看)但指向相同TControl

注意:下面的示例显示 2 如何IControl不同(从指针视图)但仍然指向同一个对象:

procedure TForm.Button1Click(Sender: TObject);
var LFrame: Tframe;
    Lcontrol: Tcontrol;
    LIcontrol1: Icontrol;
    LIcontrol2: Icontrol;
begin
  Lframe := Tframe.Create(nil);
  Lcontrol := Lframe;
  LIcontrol1 := Lframe;
  LIcontrol2 := Lcontrol;
  if LIcontrol1 <> LIcontrol2 then
    raise Exception.Create('Boom');
end;
Run Code Online (Sandbox Code Playgroud)

现在修复这个错误的好方法是什么?

Dal*_*kar 9

直接比较接口的问题是每个类都可以声明接口,即使它已经在祖先中声明了。这使得重新声明的接口可以在派生类中实现不同的方法。

每个对象实例都附有关联的元数据、接口表。接口表包含每个声明的接口的指针列表,这些指针指向该特定接口的虚拟方法表。如果接口被声明多次,则每个声明都会在接口表中拥有自己的条目,指向其自己的 VMT。

当您获取特定对象实例的接口引用时,该引用中的值是该对象接口表中的相应条目。由于该表可能包含同一接口的多个条目,因此即使它们属于同一对象,这些值也可能不同。

在 Firemonkey 的上下文中,TControl声明了IControl接口,但TFrame它的后代TControl也声明了它。这意味着TFrame实例在其接口表中将有两个不同的IControl接口条目。

TControl = class(TFmxObject, IControl, ...

TFrame = class(TControl, IControl)
Run Code Online (Sandbox Code Playgroud)

TFrame重新声明IControl接口,因为它实现了不同的GetVisible方法,出于表单设计器的目的,该方法在祖先类中声明为非虚拟方法。

如果 FMX 层次结构中的每个类仅声明IControl一次,则可以进行简单的比较,如SetHovered就可以正常工作。但如果不是,那么对于同一个对象,比较可能会返回 true。

解决方案是删除额外的接口声明(这也需要实现GetVisible为虚拟),或者将接口类型转换为对象并比较对象,或者类型转换为IUnknown,但从性能角度来看,类型转换是较慢的解决方案。然而,类型转换为对象或IUnknown是最好的快速修复,因为它不可能破坏任何其他东西,并且它不是接口破坏性更改。

演示 FMX 类中发生的情况的小示例TControl以及TFrame

type
  IControl = interface
    ['{95283CFD-F85E-4344-8577-6A6CA1C20D00}']
    procedure Print();
  end;

  TBase = class(TInterfacedObject, IControl)
  public
    procedure Print();
  end;

  TDerived = class(TBase, IControl)
  public
    procedure Print();
  end;

procedure TBase.Print;
begin
  Writeln('BASE');
end;

procedure TDerived.Print;
begin
  Writeln('DERIVED');
end;

procedure Test;
var
  Obj: TBase;
  Intf1, Intf2: IControl;
begin
  Obj := TDerived.Create;
  // Obj is declared as TBase so assigning will use IControl entry associated with TBase class
  Intf1 := Obj;
  // Typecasting to TDerived will use IControl entry associated with TDerived class
  Intf2 := TDerived(Obj);

  Writeln(Intf1 = Intf2);
  Writeln(TObject(Intf1) = TObject(Intf2));
  Writeln(Intf1 as IUnknown = Intf2 as IUnknown);

  Intf1.Print;
  Intf2.Print;
end;
Run Code Online (Sandbox Code Playgroud)

如果运行上面的代码,输出将是:

FALSE
TRUE
TRUE
BASE
DERIVED
Run Code Online (Sandbox Code Playgroud)

这表明 Intf1 和 Intf2 直接作为指针比较时是不同的。当转换回拥有对象实例时,它们指向同一个对象。与遵循 COM 指南相比,相同的 COM 对象必须返回相同的接口IUnknown它们是相等的(由相同的对象支持)。

IUnknown 查询接口

对于任何给定的 COM 对象(也称为 COM 组件),对任何对象接口上的 IUnknown 接口的特定查询必须始终返回相同的指针值。这使得客户端能够通过使用 IID_IUnknown 调用 QueryInterface 并比较结果来确定两个指针是否指向同一组件。具体来说,查询除 IUnknown 之外的接口(即使是通过相同指针的相同接口)必须返回相同的指针值。