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

What is a good way to compare 2 interfaces (IControl)? Is this a bug in Delphi?

在Delphi的源代码中,我在FMX.Forms单元看到这个:

procedure TCommonCustomForm.SetHovered(const Value: IControl);
begin
  if (Value <> FHovered) then
  begin
    ....
  end;
end;

我认为 Value <> FHovered 从根本上是错误的 因为 Value <> FHovered 可以 return 为真并且同时 Value并且 FHovered 可以指向相同的 TControl 对象。我错了吗? (注意这是我调试时看到的)

现在一个小问题:为什么2个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;

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

直接比较接口的问题是每个 class 都可以声明接口,即使它已经在祖先中声明了。这允许重新声明的接口可以在派生的 class.

中实现不同的方法

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

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

在 Firemonkey 的上下文中,TControl 声明了 IControl 接口,但是从 TControl 派生的 TFrame 也声明了它。这意味着 TFrame 个实例将在其接口 table.

中有两个不同的 IControl 接口条目
TControl = class(TFmxObject, IControl, ...

TFrame = class(TControl, IControl)

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

如果 FMX 层次结构中的每个 class 只声明一次 IControl,那么像 SetHovered 中的简单比较就可以正常工作。但如果不是,那么对于同一对象,比较可能 return 为真。

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

演示 FMX classes TControlTFrame

中发生的事情的小示例
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;

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

FALSE
TRUE
TRUE
BASE
DERIVED

这说明Intf1和Intf2当直接作为指针比较时是不同的。当转换回拥有对象实例时,它们指向同一个对象。 并且在遵循 COM 指南进行比较时,对于哪些状态,相同的 COM 对象必须 return 相同的接口 IUnknown 它们是相等的(由相同的对象支持)。

IUnknown QueryInterface

For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any of the object's interfaces must always return the same pointer value. This enables a client to determine whether two pointers point to the same component by calling QueryInterface with IID_IUnknown and comparing the results. It is specifically not the case that queries for interfaces other than IUnknown (even the same interface through the same pointer) must return the same pointer value.