【问题标题】:What is a good way to compare 2 interfaces (IControl)? Is this a bug in Delphi?比较 2 个接口(IControl)的好方法是什么?这是德尔福的一个错误吗?
【发布时间】:2022-01-19 14:44:54
【问题描述】:

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

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

我认为Value &lt;&gt; FHovered 根本上是错误的,因为Value &lt;&gt; FHovered 可以返回true,同时ValueFHovered 都可以指向同一个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;

现在还有什么好的方法可以修复这个错误?

【问题讨论】:

  • 我怀疑调试器中的错误,但除了声称我们没有任何证据。
  • @RemyLebeau 实际上,这种情况是可能的。 TControl = class(TFmxObject, IControl,TFrame = class(TControl, IControl)
  • @RemyLebeau 喜欢 Dalija 说是的,这是可能的,我看到了(顺便说一下 Tframe)。如果在 Tframe 内,在 Tcontrol 实现中(Tframe 从 Tcontrol 下降)我做 MyIcontrol1 := self 和 Tframe 外我做 MyIcontrol2 := MyTframe 然后 MyIcontrol1 MyIcontrol2 :(
  • @UweRaabe 我用一个样本编辑了这个问题,表明它是可能的
  • 我打开了一个质量错误请求:quality.embarcadero.com/browse/RSP-36612

标签: delphi firemonkey


【解决方案1】:

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

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

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

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

TControl = class(TFmxObject, IControl, ...

TFrame = class(TControl, IControl)

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

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

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

演示 FMX 类 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 对象必须为 IUnknown 返回相同的接口,它们是相等的(由相同的对象支持)。

IUnknown QueryInterface

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

【讨论】:

  • 这本质上是对象切片吗?其他类型的切片(“接口切片”)?如果是这样,加入适当的术语可能有助于提高知名度。
  • @outis 我不精通 C++,但 AFAIK 这不是切片。没有信息丢失,因为 Delphi 对象总是在堆上分配。只是您可以拥有指向同一对象中不同“入口”点的指针(引用)。对象引用是一个,每个声明的接口都添加了另一个入口点 - 您可以从中通过适当的探测和转换访问整个对象。这里的问题是接口被声明了两次,这会为对象创建两个单独的入口点,无法将它们作为指针进行比较以确定支持对象是否相同。
猜你喜欢
  • 2010-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-26
  • 1970-01-01
  • 2014-10-06
  • 2015-01-04
  • 2018-08-18
相关资源
最近更新 更多