在Delphi中,如何检查IInterface引用是否实现派生但未明确支持的接口?

Dav*_*vid 12 delphi winapi types interface delphi-2007

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

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

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

TImplementation = Class(TInterfacedObject, IDerived)
End;
Run Code Online (Sandbox Code Playgroud)

以下代码打印'Bad!' -

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

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

TImplementation = Class(TInterfacedObject, IDerived, IBase)
Run Code Online (Sandbox Code Playgroud)

然而,即使没有这样做,我已经知道 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;
Run Code Online (Sandbox Code Playgroud)

但是当我们开始将IBases放入通用容器时,我们遇到了一个问题 - 例如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;
Run Code Online (Sandbox Code Playgroud)

我非常想要某种断言来捕获任何不匹配的类型 - 这种事情可以使用Is运算符来完成,但这对接口不起作用.由于各种原因,我不希望将IBase显式添加到支持的接口列表中.有没有什么方法可以用这样的方式编写TImplementation和断言,如果硬编译IBase(List [0])是安全的事情,它会评估为真?

编辑:

正如其中一个答案所示,我添加了两个主要原因,我不想将IBase添加到TImplementation实现的接口列表中.

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

Supports (List[0], IBase)
Run Code Online (Sandbox Code Playgroud)

返回true,这并不意味着执行强制转换是安全的.QueryInterface可以返回不同的指针以满足所请求的接口.例如,如果TImplementation显式实现了IBase和IDerived(以及IInterface),则断言将成功传递:

Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase
Run Code Online (Sandbox Code Playgroud)

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

List.Add(Item As IInterface);
Run Code Online (Sandbox Code Playgroud)

断言仍然通过 - 该项仍然实现了IBase,但添加到列表中的引用只是一个IInterface - 将其强制转换为IBase不会产生任何合理的,因此断言不足以检查以下是否演员是安全的.保证工作的唯一方法是使用as-cast或支持:

(List[0] As IBase).DoWhatever;
Run Code Online (Sandbox Code Playgroud)

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

第二个原因是我希望能够比较引用的相等性,但是如果相同的实现对象由具有不同VMT偏移的不同引用保持,则无法完成.

编辑2:通过示例扩展了上述编辑.

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

话虽如此,如果有人可以完全避免这个问题,没有性能成本,也没有类型检查的危险,那将是巨大的!

Rob*_*edy 12

你可以做的一件事是停止类型转换接口.你并不需要做的是从去IDerivedIBase了,你不需要它,从去IBaseIUnknown,无论是.对a的任何引用IDerived IBase已经存在,因此IBase即使没有类型转换也可以调用方法.如果你减少了类型转换,你可以让编译器为你做更多工作并捕捉不合理的东西.

您声明的目标是能够检查您从列表中获取的内容是否真的是一个IBase参考.添加IBase为已实现的界面可以让您轻松实现该目标.从那个角度来看,你不这样做的"两个主要原因"就是没有水.

  1. "我希望能够比较平等的参考":没问题.COM要求如果QueryInterface在同一对象上使用相同的GUID 调用两次,则两次都会获得相同的接口指针.如果你有两个任意的接口引用,并且你as将它们都转换为IBase,那么当且仅当它们由同一个对象支持时,结果将具有相同的指针值.

    由于您似乎希望您的列表仅包含IBase值,并且您没有通用TInterfaceList<IBase>对其有用的Delphi 2009 ,因此您可以自我约束以始终IBase向列表中显式添加值,而不是任何后代类型的值.每当您向列表中添加项目时,请使用以下代码:

    List.Add(Item as IBase);
    
    Run Code Online (Sandbox Code Playgroud)

    这样,列表中的任何重复项都很容易被发现,并且您的"强硬演员"可以确保工作.

  2. "它实际上并没有解决问题":但鉴于上述规则,它确实如此.

    Assert(Supports(List[i], IBase));
    
    Run Code Online (Sandbox Code Playgroud)

    当对象显式实现其所有接口时,您可以检查这样的事情.如果你像上面所描述的那样将项目添加到列表中,则可以安全地禁用断言.通过启用断言,您可以检测有人在程序中的其他位置更改了代码,从而错误地将项目添加到列表中.经常运行您的单元测试也可以让您在问题出现后立即检测到它.

考虑到上述要点,您可以检查添加到列表中的任何内容是否已使用以下代码正确添加:

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.
Run Code Online (Sandbox Code Playgroud)

如果断言失败,那么您在列表中添加了一些根本不支持IBase的内容,或者您​​为某个对象添加的特定接口引用不能作为IBase引用.如果断言通过,那么你知道这List[i]会给你一个有效的IBase值.

请注意,添加到列表中的值不需要IBase显式值.鉴于上面的类型声明,这是安全的:

var
  A: IDerived;
begin
  A := TImplementation.Create;
  List.Add(A);
end;
Run Code Online (Sandbox Code Playgroud)

这是安全的,因为通过TImplementation形成一个继承树实现的接口退化为一个简单的列表.没有分支,其中两个接口不相互继承但具有共同的祖先.如果有两个后代IBase,并且TImplementation两者都实现了它们,则上述代码将无效,因为IBase保留的引用A不一定是该IBase对象的"规范" 引用.断言会检测到该问题,而您需要添加它List.Add(A as IBase).

当您禁用断言时,只有在添加到列表时才支付获得类型权限的成本,而不是在从列表中读取时.我将变量命名AssertionItem为阻止您在过程中的其他位置使用该变量; 它仅用于支持断言,一旦断言被禁用,它就没有有效值.


Mar*_*ntù 6

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

TImplementation = Class(TInterfacedObject, IDerived, IBase)
Run Code Online (Sandbox Code Playgroud)

或者只是你不喜欢它?

进一步评论

你永远不应该,甚至硬类型转换界面.当你在界面上执行"as"时,它会以正确的方式调整对象vtable指针...如果你进行硬转换(并且有方法可以调用),你的代码很容易崩溃.我的印象是你正在处理像对象这样的接口(使用继承和转换以相同的方式),而它们的内部工作确实不同!