12

如果我有以下接口和实现它们的类 -

IBase = Interface ['{82F1F81A-A408-448B-A194-DCED9A7E4FF7}']
End;

IDerived = Interface(IBase) ['{A0313EBE-C50D-4857-B324-8C0670C8252A}']
End;

TImplementation = Class(TInterfacedObject, IDerived)
End;

下面的代码打印出'Bad!' -

Procedure Test;
Var
    A : IDerived;
Begin
    A := TImplementation.Create As IDerived;
    If Supports (A, IBase) Then
        WriteLn ('Good!')
    Else
        WriteLn ('Bad!');
End;

这有点烦人但可以理解。支持无法转换为 IBase,因为 IBase 不在 TImplementation 支持的 GUID 列表中。可以通过将声明更改为 -

TImplementation = Class(TInterfacedObject, IDerived, IBase)

然而,即使不这样做,我也已经知道A 实现了 IBase,因为 A 是一个 IDerived,而 IDerived 是一个 IBase。因此,如果我遗漏支票,我可以投 A,一切都会好起来的 -

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;

但是当我们开始将 IBase 放入一个通用容器(例如 TInterfaceList)时,我们遇到了一个问题。它只能容纳 IInterfaces,所以我们必须做一些转换。

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;

我非常希望有某种断言来捕获任何不匹配的类型——这种事情可以使用 Is 运算符对对象完成,但这不适用于接口。由于各种原因,我不想将 IBase 显式添加到支持的接口列表中。有什么方法可以编写 TImplementation 和断言,使其评估为真 iff 硬铸造 IBase(List[0]) 是安全的事情吗?

编辑:

正如答案之一中出现的那样,我添加了两个主要原因,我不想将 IBase 添加到 TImplementation 实现的接口列表中。

首先,它实际上并没有解决问题。如果,在 Test2 中,表达式:

Supports (List[0], IBase)

返回 true,这并不意味着执行硬转换是安全的。QueryInterface 可能会返回一个不同的指针来满足请求的接口。例如,如果 TImplementation 显式实现了 IBase 和 IDerived(以及 IInterface),那么断言将成功通过:

Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase

但是想象一下,有人错误地将一个项目作为 IInterface 添加到列表中

List.Add(Item As IInterface);

断言仍然通过 - 该项目仍然实现 IBase,但添加到列表中的引用仅是 IInterface - 将其硬转换为 IBase 不会产生任何合理的结果,因此断言不足以检查以下是否硬 -演员是安全的。保证工作的唯一方法是使用铸态或支持:

(List[0] As IBase).DoWhatever;

但这是一个令人沮丧的性能成本,因为它的目的是由代码将项目添加到列表中以确保它们属于 IBase 类型 - 我们应该能够假设这一点(因此,如果这个假设是,则要捕获的断言错误的)。断言甚至不是必需的,除非有人更改某些类型时捕获以后的错误。这个问题来自的原始代码对性能也相当关键,因此我宁愿避免实现很少的性能成本(它仍然只能在运行时捕获不匹配的类型,但无法编译更快的发布版本) .

第二个原因是我希望能够比较引用是否相等,但如果相同的实现对象由具有不同 VMT 偏移的不同引用持有,则无法做到这一点。

编辑 2:用一个例子扩展了上面的编辑。

编辑3:注意:问题是我如何制定断言,以便在断言通过时硬转换是安全的,而不是如何避免硬转换。有一些方法可以不同地执行硬转换步骤,或者完全避免它,但是如果存在运行时性能成本,我就不能使用它们。我想要在断言中检查的所有成本,以便以后可以编译出来。

话虽如此,如果有人可以在没有性能成本和类型检查危险的情况下完全避免这个问题,那就太好了!

4

3 回答 3

12

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.

  1. "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 you as-cast them both to IBase, 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 generic TInterfaceList<IBase> would be helpful, you can discipline yourself to always explicitly add IBase 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.

  2. "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.

于 2009-04-16T12:54:49.243 回答
6

您的考试是正确的,据我所知,您遇到的问题确实没有直接的解决方案。原因在于接口之间继承的性质,它与类之间的继承只有模糊的相似之处。继承的接口是一个全新的接口,它与继承的接口有一些共同的方法,但没有直接的联系。因此,通过选择不实现基类接口,您做出了编译程序将遵循的特定假设:TImplementation 不实现 IBase。我认为“接口继承”有点用词不当,接口扩展更有意义!一种常见的做法是让基类实现基接口,然后派生类实现扩展接口,但如果您想要一个实现两者的单独类,只需列出这些接口。您要避免使用的特定原因是:

TImplementation = Class(TInterfacedObject, IDerived, IBase)

或者只是你不喜欢它?

进一步评论

你不应该,甚至硬类型转换一个接口。当您在接口上执行“as”时,它将以正确的方式调整对象 vtable 指针......如果您进行硬转换(并且有方法可以调用),您的代码很容易崩溃。我的印象是您将接口视为对象(以相同的方式使用继承和强制转换),而它们的内部工作方式确实不同!

于 2009-04-16T07:31:39.560 回答
0

在测试2中;

您不应该通过 IBase(A) 将 IDerived 重新键入为 IBase,但要使用:

Supports(A, IBase, B);

添加到列表可以只是:

List.Add(B);
于 2009-04-16T07:27:10.117 回答