问题描述
如果我有以下接口和实现它们的类 -
IBase = Interface ['{82F1F81A-A408- 448B-A194-DCED9A7E4FF7}']
结束;
IDerived = Interface(IBase)['{A0313EBE-C50D-4857-B324-8C0670C8252A}']
结束;
TImplementation = Class(TInterfacedObject,IDerived)
End;
以下代码打印Bad! -
程序测试;
Var
A:IDerived;
开始
A:= TImplementation.Create As IDerived;
如果支持(A,IBase)然后
WriteLn('Good!')
Else
WriteLn('Bad!');
结束;
这有点烦人,但可以理解。支持不能转换为IBase,因为IBase不在TImplementation支持的GUID列表中。可以通过将声明更改为 -
TImplementation = Class(TInterfacedObject,IDerived,IBase)
即使没有这样做,我已经知道 A实现了IBase,因为A是一个IDerived和一个IDerived是一个IBase。所以如果我不用支票我可以投A,一切都会很好 -
程序测试;
Var
A:IDerived;
B:IBase;
开始
A:= TImplementation.Create As IDerived;
B:= IBase(A);
//现在可以成功调用B的任何方法
结束;
但是,当我们开始将IBases放入通用容器(例如TInterfaceList)时,我们遇到了一个问题。它只能保留IInterfaces,所以我们必须做一些转换。
过程Test2;
Var
A:IDerived;
B:IBase;
列表:TInterfaceList;
开始
A:= TImplementation.Create As IDerived;
B:= IBase(A);
列表:= TInterfaceList.Create;
List.Add(IInterface(B));
Assert(Supports(List [0],IBase)); //这个断言失败
IBase(List [0])。DoWhatever; //假设我在IBase中宣布DoWhatever,这可以正常工作,但它不是类型安全的
List.Free;
结束;
我非常希望有某种断言来捕捉任何不匹配的类型 - 这种事情可以使用Is操作符完成对象,但这对于接口不起作用。由于种种原因,我不想明确地将IBase添加到支持的接口列表中。有没有办法我可以写TImplementation和断言这样一个方式,它将评估为true如果硬化的IBase(List [0])是一件安全的事情?
编辑:
正如在其中一个答案中,我添加了两个主要原因要将IBase添加到TImplementation实现的接口列表中。
首先,它并不能解决问题。如果在Test2中,表达式:
支持(List [0],IBase)
返回true,这并不意味着执行硬执行是安全的。 QueryInterface可能会返回一个不同的指针来满足请求的接口。例如,如果TImplementation显式实现了IBase和IDerived(和IInterface),则断言将成功传递:
Assert(Supports (List [0],IBase)); // Passes,List [0]确实实现了IBase
但是想像有人误将一个项目添加到列表为IInterface
List.Add(Item As IInterface);
断言仍然通过 - 项目仍然实现IBase,但添加到列表中的引用是IInterface只有 - 将其硬化铸造到IBase不会产生任何明智的东西,所以在检查以下硬铸是否安全的情况下,这种说法是不够的。确保工作的唯一方法是使用演播或支持:
(List [0] As IBase)无论如何
但这是一个令人沮丧的性能成本,因为它是为了添加代码的责任到列表中,以确保它们是IBase类型 - 我们应该能够假设这一点(因此,假设这个假设是假的话)。断言甚至不是必要的,除非有人改变某些类型,否则要抓住以后的错误。这个问题来自的原始代码对于性能也是非常关键的,所以性能成本很少(在运行时仍然只能捕获不匹配的类型,但是没有编译更快版本的可能性)是我宁愿避免的
第二个原因是我想能够比较引用的相等性,但是如果同一个实现对象由不同的引用带有不同的引用而不能完成VMT偏移。
编辑2:以示例展开上述编辑。
编辑3:注意:问题是如何制定断言,以便强制转换是安全的,如果断言通过,而不是如何避免强硬。有一些方法可以不同地进行强硬步骤,或者完全避免这种情况,但是如果运行时性能成本,我就不能使用它们。我想要在断言中检查所有费用,以便以后可以编辑。
说到这一点,如果有人可以完全避免这个问题,没有性能成本,没有类型检查的危险会很棒!
你可以做的一件事是停止类型转换接口。您不需要从 IDerived
到 IBase
,而不需要它去从 IBase
到 IUnknown
。任何引用 IDerived
已经是 IBase
,所以你可以调用 IBase
方法即使没有类型转换。如果你做的不那么简单,你可以让编译器为你做更多的工作,并且抓住那些不健全的东西。
你的目标是能够检查您从列表中得到的东西真的是一个 IBase
引用。添加 IBase
作为实现的界面将允许您轻松实现该目标。在这方面,你没有这样做的两个主要原因没有任何水。
-
我想能够比较参考平等:没问题。 COM要求,如果您在同一对象上使用相同的GUID调用
QueryInterface
两次,那么您将获得两次相同的接口指针。如果你有两个任意的接口引用,并且你将作为
将它们都连接到IBase
,那么结果将由于您似乎希望您的列表只包含IBase $,因此您只需要包含
IBase
值,并且您没有Delphi 2009中通用的TInterfaceList< IBase>
将会有所帮助,您可以遵守自己始终显式添加IBase
值列表,从不为任何后代类型的值。每当您将一个项目添加到列表中时,请使用以下代码:List.Add(Item as IBase);
这样,列表中的任何重复项都很容易检测,您的硬转型
-
实际上并不解决问题:但是,根据上面的规则,
Assert(Supports(List [i],IBase));
当对象显式实现其所有接口时,您可以检查这样的事情。如果您已将项目添加到列表中,如上所述,可以禁用该断言。启用断言可让您检测某人何时更改程序中其他位置的代码,以将错误的项添加到列表中。经常运行您的单元测试,您也可以在介绍之后尽快检测问题。
记住,您可以检查添加到列表中的任何内容是否正确添加了以下代码:
var
AssertionItem:IBase;
Assert(Supports(List [i],IBase,AssertionItem)
和(AssertionItem = List [i]));
//我不记得编译器是否接受将IBase
//值(AssertionItem)与IUnknown值(List [i])进行比较。如果
//编译器抱怨,则只需将声明更改为
// IUnknown;支持功能不会注意到。
如果断言失败,则您添加了不支持 IBase
,或者您为某个对象添加的特定接口引用不能作为 IBase
引用。如果断言通过,那么您知道 List [i]
将为您提供一个有效的 IBase
值。
请注意,添加到列表中的值不会需要成为 IBase
值。鉴于上面的类型声明,这是安全的:
var
A:IDerived;
begin
A:= TImplementation.Create;
List.Add(A);
结束
这是安全的,因为 TImplementation
形成一个退化为简单列表的继承树。没有分支,其中两个接口不相互继承但具有共同的祖先。如果 IBase
和 TImplementation
的两个因为 A
中保存的 IBase
引用不一定是规范 IBase
该对象的引用。该断言会检测到该问题,您需要添加 List.Add(A as IBase)
。
当您禁用断言时,只有在添加到列表中时才会收到正确的类型的费用,而不是从列表中读取。我命名变量 AssertionItem
以阻止您在该过程的其他位置使用该变量;它只是支持断言,一旦断言被禁用,它就不会有有效的值。
If I have the following interfaces and a class that implements them -
IBase = Interface ['{82F1F81A-A408-448B-A194-DCED9A7E4FF7}']
End;
IDerived = Interface(IBase) ['{A0313EBE-C50D-4857-B324-8C0670C8252A}']
End;
TImplementation = Class(TInterfacedObject, IDerived)
End;
The following code prints 'Bad!' -
Procedure Test;
Var
A : IDerived;
Begin
A := TImplementation.Create As IDerived;
If Supports (A, IBase) Then
WriteLn ('Good!')
Else
WriteLn ('Bad!');
End;
This is a little annoying but understandable. Supports can't cast to IBase because IBase is not in the list of GUIDs that TImplementation supports. It can be fixed by changing the declaration to -
TImplementation = Class(TInterfacedObject, IDerived, IBase)
Yet even without doing that I already know that A implements IBase because A is an IDerived and an IDerived is an IBase. So if I leave out the check I can cast A and everything will be fine -
Procedure Test;
Var
A : IDerived;
B : IBase;
Begin
A := TImplementation.Create As IDerived;
B := IBase(A);
//Can now successfully call any of B's methods
End;
But we come across a problem when we start putting IBases into a generic container - TInterfaceList for example. It can only hold IInterfaces so we have to do some casting.
Procedure Test2;
Var
A : IDerived;
B : IBase;
List : TInterfaceList;
Begin
A := TImplementation.Create As IDerived;
B := IBase(A);
List := TInterfaceList.Create;
List.Add(IInterface(B));
Assert (Supports (List[0], IBase)); //This assertion fails
IBase(List[0]).DoWhatever; //Assuming I declared DoWhatever in IBase, this works fine, but it is not type-safe
List.Free;
End;
I would very much like to have some sort of assertion to catch any mismatched types - this sort of thing can be done with objects using the Is operator, but that doesn't work for interfaces. For various reasons, I don't want to explicitly add IBase to the list of supported interfaces. Is there any way I can write TImplementation and the assertion in such a way that it will evaluate to true iff hard-casting IBase(List[0]) is a safe thing to do?
Edit:
As it came up in the one of the answers, I'm adding the two major reasons I do not want to add IBase to the list of interfaces that TImplementation implements.
Firstly, it doesn't actually solve the problem. If, in Test2, the expression:
Supports (List[0], IBase)
returns true, this does not mean it is safe to perform a hard-cast. QueryInterface may return a different pointer to satisfy the requested interface. For example, if TImplementation explicitly implements both IBase and IDerived (and IInterface), then the assertion will pass successfully:
Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase
But imagine that somebody mistakenly adds an item to the list as an IInterface
List.Add(Item As IInterface);
The assertion still passes - the item still implements IBase, but the reference added to the list is an IInterface only - hard-casting it to an IBase would not produce anything sensible, so the assertion isn't sufficient in checking whether the following hard-cast is safe. The only way that's guaranteed to work would be to use an as-cast or supports:
(List[0] As IBase).DoWhatever;
But this is a frustrating performance cost, as it is intended to be the responsibility of the code adding items to the list to ensure they are of the type IBase - we should be able to assume this (hence the assertion to catch if this assumption is false). The assertion isn't even necessary, except to catch later mistakes if anyone changes some of the types. The original code this problem comes from is also fairly performance critical, so a performance cost that achieves little (it still only catches mismatched types at run-time, but without the possibility to compile a faster release build) is something I'd rather avoid.
The second reason is I want to be able to compare references for equality, but this can't be done if the same implementation object is held by different references with different VMT offsets.
Edit 2: Expanded the above edit with an example.
Edit 3: Note: The question is how can I formulate the assertion so that the hard-cast is safe iff the assertion passes, not how to avoid the hard-cast. There are ways to do the hard-cast step differently, or to avoid it completely, but if there is a runtime performance cost, I can't use them. I want all the cost of checking within the assertion so that it can be compiled out later.
Having said that, if someone can avoid the problem altogether with no performance cost and no type-checking danger that would be great!
One thing you can do is stop type-casting interfaces. You don't need to do it to go from IDerived
to IBase
, and you don't need it to go from IBase
to IUnknown
, either. Any reference to an IDerived
is an IBase
already, so you can call IBase
methods even without type-casting. If you do less type-casting, you let the compiler do more work for you and catch things that aren't sound.
Your stated goal is to be able to check that the thing you're getting out of your list really is an IBase
reference. Adding IBase
as an implemented interface would allow you to achieve that goal easily. In that light, your "two major reasons" for not doing that don't hold any water.
"I want to be able to compare references for equality": No problem. COM requires that if you call
QueryInterface
twice with the same GUID on the same object, you get the same interface pointer both times. If you have two arbitrary interface references, and youas
-cast them both toIBase
, then the results will have the same pointer value if and only if they are backed by the same object.Since you seem to want your list to only contain
IBase
values, and you don't have Delphi 2009 where a genericTInterfaceList<IBase>
would be helpful, you can discipline yourself to always explicitly addIBase
values to the list, never values of any descendant type. Whenever you add an item to the list, use code like this:List.Add(Item as IBase);
That way, any duplicates in the list are easy to detect, and your "hard casts" are assured to work.
"It doesn't actually solve the problem": But it does, given the rule above.
Assert(Supports(List[i], IBase));
When the object explicitly implements all its interfaces, you can check for things like that. And if you've added items to the list like I described above, it's safe to disable the assertion. Enabling the assertion lets you detect when someone has changed code elsewhere in your program to add an item to the list incorrectly. Running your unit tests frequently will let you detect the problem very soon after it's introduced, too.
With the above points in mind, you can check that anything that was added to the list was added correctly with this code:
var
AssertionItem: IBase;
Assert(Supports(List[i], IBase, AssertionItem)
and (AssertionItem = List[i]));
// I don't recall whether the compiler accepts comparing an IBase
// value (AssertionItem) to an IUnknown value (List[i]). If the
// compiler complains, then simply change the declaration to
// IUnknown instead; the Supports function won't notice.
If the assertion fails, then either you added something to the list that doesn't support IBase
at all, or the specific interface reference you added for some object cannot serve as the IBase
reference. If the assertion passes, then you know that List[i]
will give you a valid IBase
value.
Note that the value added to the list doesn't need to be an IBase
value explicitly. Given your type declarations above, this is safe:
var
A: IDerived;
begin
A := TImplementation.Create;
List.Add(A);
end;
That's safe because the interfaces implemented by TImplementation
form an inheritance tree that degenerates to a simple list. There are no branches where two interfaces don't inherit from each other but have a common ancestor. If there were two decendants of IBase
, and TImplementation
implemented them both, the above code wouldn't be valid because the IBase
reference held in A
wouldn't necessarily be the "canonical" IBase
reference for that object. The assertion would detect that problem, and you'd need to add it with List.Add(A as IBase)
instead.
When you disable assertions, the cost of getting the types right is paid only while adding to the list, not while reading from the list. I named the variable AssertionItem
to discourage you from using that variable elsewhere in the procedure; it's there only to support the assertion, and it won't have a valid value once assertions are disabled.
这篇关于在Delphi中,如何检查IInterface引用是否实现派生但不明确支持的接口?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!