In Delphi, how can you check if an IInterface reference implements a derived but not explicitly-supported interface?

痞子三分冷 提交于 2019-12-05 00:29:52

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.

You are right in your examination and as far as I can tell there is really no direct solution to the problem you've encountered. The reasons lies in the nature of inheritance among interfaces, which has only a vague resemblance of inheritance among classes. An inherited interfaces is a brand new interface, that has some methods in common with the interface it inherits from, but no direct connection. So by choosing not to implement the base class interface, you are making a specific assumption that the compiled program will follow: TImplementation does not implement IBase. I think "interface inheritance" is somewhat of a misnomer, interface extension makes more sense! A common practice is to have a base class implementing the base interface, and than derived classes implementing the extended interfaces, but in case you want a separate class that implements both simply list those interfaces. It there a specific reason you want to avoid using:

TImplementation = Class(TInterfacedObject, IDerived, IBase)

or just you don't like it?

Further Comment

You should never, even hard type cast an interface. When you do "as" on an interface it will adjust the object vtable pointers in the right way... if you do a hard cast (and have methods to call) you code can easily crash. My impression is that you are treating interfaces like objects (using inheritance and casts in the same way) while their internal working is really different!

In Test2;

You shound't retype IDerived as IBase by IBase(A) but with:

Supports(A, IBase, B);

And adding to list can be just:

List.Add(B);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!