Delphi接口继承:为什么我无法访问祖先接口的成员?

11
假设您有以下内容:
//Note the original example I posted didn't reproduce the problem so
//I created an clean example  
  type
    IParent = interface(IInterface)
    ['{85A340FA-D5E5-4F37-ABDD-A75A7B3B494C}']
      procedure DoSomething;
    end;

    IChild = interface(IParent)
    ['{15927C56-8CDA-4122-8ECB-920948027015}']
      procedure DoSomethingElse;
    end;

    TGrandParent = class(TInterfacedObject)
    end;

    TParent = class(TGrandParent)
    end;

    TChild = class(TParent, IChild)
    private
      FChildDelegate: IChild;
    public
      property ChildDelegate:IChild read FChildDelegate implements IChild;
    end;

    TChildDelegate = class(TInterfacedObject, IChild)
    public
      procedure DoSomething;
      procedure DoSomethingElse;
    end;

我认为这将允许您调用DoSomething,但事实并非如此:

procedure CallDoSomething(Parent: TParent);
begin
  if Parent is TChild then
    TChild(Parent).DoSomething;
end;

很明显编译器正在强制执行接口继承,因为除非实现了IParent的成员,否则两个类都无法编译。尽管如此,编译器在实例化和使用类时无法解析IParent的成员。 我可以通过在TMyClass的类声明中显式包含IParent来解决这个问题:
TMyClass = class(TInterfacedObject, IChild, IParent)

算了,这不能解决任何问题。


1
我们可以假设FObject应该是FChild吗? - Lieven Keersmaekers
2
将“property Object”更改为“property Obj”后,代码可以在Delphi 2009中编译。 - kludg
@Serg 我在发布之前更改了所有标识符的名称。原始名称不是 Object - Kenneth Cochran
将你的代码更改为显示真正的问题(正如dthorpe所说,该问题确实存在)。在修复明显的语法错误后,您当前的代码已编译。 - kludg
5
我有点困惑。目前提供的代码是否实际上展示了所描述的问题?请不要更改标识符的名称。复制并粘贴您的真实代码。(如果您不想透露标识符的名称,则在IDE中更改它们,确认代码仍然展示问题,然后复制并粘贴代码。展示虚假代码会浪费所有人的时间。) - Rob Kennedy
显示剩余2条评论
4个回答

16
如果一个实现类没有声明支持继承接口,那么该类将无法与继承接口的变量分配兼容。您发布的代码示例应该可以正常工作(使用IChild接口),但是如果您尝试从TMyClass的实例分配给IParent类型变量,则会遇到问题。
原因是因为COM和ActiveX允许实现实现一个派生接口(即您的IChild),但否认该接口的祖先(即IParent)。由于Delphi接口旨在与COM兼容,因此这就是这种古怪的现象产生的原因。
我很确定我大约10到12年前写了一篇关于此主题的文章,但我的Borland博客没有经过Embarcadero服务器的转换而幸存下来。
可能会有编译器指令来更改这种行为,但我不记得了。

@Lieven:任何被COM接触的东西都不仅仅是“语法糖”。:P - dthorpe
@dthorpe 请看我的更新问题。我怀疑问题实际上可能是我正在尝试通过对象引用而不是接口引用访问委托方法。 - Kenneth Cochran
1
@所有人:问题显然是TChild没有一个名为“DoSomething”的方法——它将实现(因此也是提供)委托给了它的IChild。因此,要调用由TChild实例提供的IChild.DoSomething,您必须获取对该实例的委托的引用,或者通过请求时将产生的IChild接口引用来简化接口。有关详细信息,请参见我的答案。 - Deltics
1
顺便说一下,我找到了这个链接,它似乎与你写的文章有关。我在互联网档案馆上找不到原始文章。 - Kenneth Cochran
2
实际上,你的 dcc 博客已经被存档在 Wayback Machine 上了:http://web.archive.org/web/*/http://blogs.borland.com/dcc - Jeroen Wiert Pluimers
显示剩余6条评论

9
问题不在接口声明或类实现中,而是在您的使用代码中:
procedure CallDoSomething(Parent: TParent);
begin
  if Parent is TChild then
    TChild(Parent).DoSomething;  // << This is wrong
end;

由于 TChild 没有一个名为 "DoSomething" 的方法,所以这不起作用。如果 TChild 直接实现了 IChild 接口,则通常情况下可能是可行的,因为 TChild 将直接实现该方法并且作为 IChild 接口的一部分。
请注意,如果 TChild私有 范围内实现了 DoSomething,则它仍将通过接口访问,但正常的作用域规则意味着您仍然无法使用 TChild 引用从类/单元外部调用它。
在您的情况下,您只需要获取适当的接口,然后通过接口调用您需要的方法即可:
  if Parent is TChild then
    (Parent as IChild).DoSomething;

然而,您正在使用类类型test来确定接口的存在,依赖于实现细节(即知道TChild实现了IChild)。我建议您应该直接使用接口测试,以将此依赖关系与这些实现细节隔离开来:

  var
    parentAsChild: IChild;

  begin
    if Parent.GetInterface(IChild, parentAsChild) then
      parentAsChild.DoSomething;
  end;

1
这才是真正的问题。按照您的建议,我发现调用点与被调用类之间的耦合过于紧密,需要扩展接口。当维护别人的代码时,就是这样的生活。 - Kenneth Cochran

0

编辑:这个答案已经不再相关,因为它是在原问题修改之前发布的。


这可以在Delphi 2010中编译:

type
  IParent = interface(IInterface)
    function DoSomething: String;
  end;

  IChild = interface(IParent)
    function DoSomethingElse: string;
  end;

  TMyClass = class(TInterfacedObject, IChild)
  private
  public
    function DoSomething: String;
    function DoSomethingElse: String;
  end;

// ... 

procedure Test;
var
  MyObject : IChild;
begin
  MyObject := TMyClass.Create;
  MyObject.DoSomething;
end;

问题在于 implements 子句。你写的与 OP 写的 TChild 类基本相同。 - Lieven Keersmaekers
我的回答是对原帖相关的,在他编辑之前。在那个示例代码中,他将变量声明为IChild,当他尝试调用DoSomething时,会收到编译器错误。 - Ville Krumlinde

-1

Delphi实现的QueryInterface不符合标准。在题为人们搞砸IUnknown :: QueryInterface的方法的博客文章中,Raymond Chen列举了常见的实现失败。最值得注意的是第三点。

Forgetting to respond to base interfaces. When you implement a derived interface, you implicitly implement the base interfaces, so don't forget to respond to them, too.

 IShellView *psv = some object;
 IOleView *pow;
 psv->QueryInterface(IID_IOleView, (void**)&pow);

Some objects forget and the QueryInterface fails with E_NOINTERFACE.

除非一个继承的接口被显式地附加到一个类或其祖先,否则Delphi将无法找到它。它只是遍历对象及其继承类型的接口表,并检查接口ID的匹配性,而不检查基本接口。


这篇分析有误。Danny 解释说 Delphi 在设计时避开了一些 MS 代码的怪异之处。QI 的实现实际上是一系列组合体。你需要通过指定支持的接口并依赖 TInterfacedObject 中给出的 QI 来实现它。不要认为这个函数是你实现 QI 的唯一部分,它依赖于你声明支持的接口。你的工作是列出继承体系。 - David Heffernan
1
@DavidHeffernan 我已经阅读并理解了他们是如何绕过 IClassFactory2 的错误的,但他们也破坏了一个类实现派生接口隐式实现其祖先的概念。 - Jasper Schellingerhout
他们没有实现QI,而你正在实现。通过你声明要实现的接口。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接