比较 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 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;
如果你运行上面的代码输出将是:
FALSE
TRUE
TRUE
BASE
DERIVED
这说明Intf1和Intf2当直接作为指针比较时是不同的。当转换回拥有对象实例时,它们指向同一个对象。
并且在遵循 COM 指南进行比较时,对于哪些状态,相同的 COM 对象必须 return 相同的接口 IUnknown
它们是相等的(由相同的对象支持)。
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.
在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 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;
如果你运行上面的代码输出将是:
FALSE
TRUE
TRUE
BASE
DERIVED
这说明Intf1和Intf2当直接作为指针比较时是不同的。当转换回拥有对象实例时,它们指向同一个对象。
并且在遵循 COM 指南进行比较时,对于哪些状态,相同的 COM 对象必须 return 相同的接口 IUnknown
它们是相等的(由相同的对象支持)。
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.