Delphi interface inheritance: Why can't I access ancestor interface's members?
Asked Answered
L

4

11

Assume you have the following:

//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;

I would think that this would allow you to call DoSomething but this doesn't seem to be the case:

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

Its clear that the compiler is enforcing the interface inheritance because neither class will compile unless the members of IParent are implemented. Despite this the compiler is unable to resolve members of the IParent when the class is instantiated and used.

I can work around this by explicitly including IParent in the class declaration of TMyClass:

TMyClass = class(TInterfacedObject, IChild, IParent)

Nevermind, this doesn't work around anything.

Loving answered 7/12, 2010 at 18:6 Comment(7)
Can we assume that FObject should be FChild?Sandler
@Lieven, Thanks. Missed that one. I copied from the original source and changed the names to make it clearer.Loving
After changing "property Object" to, for example, "property Obj" the code compiles in Delphi 2009Fivepenny
@Serg I changed the names of all the identifiers before posting. The original name wasn't Object.Loving
Change your code to show the real issue (as dthorpe says the issue exists). Your current code compiles after fixing an obvious syntax error.Fivepenny
I'm confused. Does the code in the question, as it appears right this moment, actually exhibit the described problem? Please do not change the names of your identifiers. Copy and paste your real code. (If you don't want to reveal the names of your identifiers, then change them in the IDE, confirm that your code still demonstrates the problem, and then copy and paste that code. Showing fake code wastes everyone's time.)Boys
@Rob @Serg My apologies. I made several invalid assumptions. I assumed the problem was with interface inheritance, it wasn't. From there I assumed that I had extracted the core elements of the problem, I hadn't. Finally, I assumed I translated the original code correctly, I didn't.Loving
Z
9

The problem is not in the interface declarations or class implementations, but in your consumer code:

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

Is not going to work because TChild does not have a method "DoSomething". If TChild implemented IChild directly, then this would normally be possible because TChild would implement the method directly AND as part of the IChild interface.

Note however, that if TChild implemented DoSomething in PRIVATE scope, it would remain accessible thru the interface but normal scoping rules would mean that you still couldn't invoke it (from outside the class/uni) using a TChild reference either.

In your case, you simply need to obtain the appropriate interface and then invoke the method you require thru the interface:

  if Parent is TChild then
    (Parent as IChild).DoSomething;

However, you are using a class type test to determine (infer) the presence of an interface, relying on an implementation detail (knowledge that TChild implements IChild). I suggest you should instead be using interface testing directly, to isolate this dependency from those implementation details:

  var
    parentAsChild: IChild;

  begin
    if Parent.GetInterface(IChild, parentAsChild) then
      parentAsChild.DoSomething;
  end;
Zante answered 7/12, 2010 at 20:46 Comment(1)
This is the real problem. Following your suggestion lead me to discover the call point is too tightly coupled to the called class and that the interface need to be expanded. Such is life when maintaining someone else' code.Loving
B
16

If an implementing class does not declare that it supports an inherited interface, then the class will not be assignment compatible with variables of the inherited interface. The code sample you posted should work fine (using the IChild interface), but if you try to assign from an instance of TMyClass to a variable of IParent, then you'll run into trouble.

The reason is because COM and ActiveX allow an implementation to implement a descendent interface (your IChild) but deny the ancestor of that interface (IParent). Since Delphi interfaces are intended to be COM compatible, that's where this goofy artifact comes from.

I'm pretty sure I wrote an article about this about 10 or 12 years ago, but my Borland blog did not survive the transition to the Embarcadero server.

There may be a compiler directive to change this behavior, I don't recall.

Bushey answered 7/12, 2010 at 18:44 Comment(11)
@Bushey - so the statement I grew up with that interface inheritance is just syntactic sugar to reduce typing is not entirely correct?!Sandler
This is probably the problem. The actual class is a descendant of a class hierarchy which provides default implementations for some of the interfaces. It is being passed to a function as an ancestor class and then cast to the descendant class before DoSomething is called.Loving
@Lieven: Nothing touched by COM is ever "just" syntactic sugar. :PBushey
@codeelegance: Casting up and down with class types shouldn't be a problem. The issue is with extracting an ancestor interface type from a descendent class that doesn't declare that it implements the ancestor interface. I think if an ancestor class declares that it implements the ancestor interface, your descendent class will be ok for supporting the ancestor interface.Bushey
@Bushey see my updated question. I suspect the issue may actually be that I'm attempting to access delegated methods through an object reference rather than an interface reference.Loving
@all: Surely the problem is simply that TChild doesn't have a method "DoSomething" - it delegates the implementation (and therefore the provision) of that to it's IChild, uh, delegate. So to invoke the IChild.DoSomething provided by a TChild instance you have to obtain a reference to that instance's delegate, or, as intended to be simplified by interfaces, thru the IChild interface reference that it will yield when asked. See my answer for details.Zante
Ah, well, the delegate part makes this a completely different question. Love it when the problem changes out from under you. :PBushey
Sorry about that. I Should have leaned on the the compiler when I was trying to reduce the scope of the problem. Still I gave you an upvote since your answer was the best given the question's original context.Loving
Btw, I found this, which appears to be related to the article you wrote. I couldn't find the original article using the Internet Archive.Loving
Yes, that looks like the article. There may have been an earlier article specific to Win32 because this question came up a lot back then, and long before the Delphi for .NET compiler was around. Thanks for the links.Bushey
Actually, your dcc blog is on the wayback machine: web.archive.org/web*/blogs.borland.com/dccAshes
Z
9

The problem is not in the interface declarations or class implementations, but in your consumer code:

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

Is not going to work because TChild does not have a method "DoSomething". If TChild implemented IChild directly, then this would normally be possible because TChild would implement the method directly AND as part of the IChild interface.

Note however, that if TChild implemented DoSomething in PRIVATE scope, it would remain accessible thru the interface but normal scoping rules would mean that you still couldn't invoke it (from outside the class/uni) using a TChild reference either.

In your case, you simply need to obtain the appropriate interface and then invoke the method you require thru the interface:

  if Parent is TChild then
    (Parent as IChild).DoSomething;

However, you are using a class type test to determine (infer) the presence of an interface, relying on an implementation detail (knowledge that TChild implements IChild). I suggest you should instead be using interface testing directly, to isolate this dependency from those implementation details:

  var
    parentAsChild: IChild;

  begin
    if Parent.GetInterface(IChild, parentAsChild) then
      parentAsChild.DoSomething;
  end;
Zante answered 7/12, 2010 at 20:46 Comment(1)
This is the real problem. Following your suggestion lead me to discover the call point is too tightly coupled to the called class and that the interface need to be expanded. Such is life when maintaining someone else' code.Loving
B
0

edit: This answer is no longer relevant because it was posted before the original question got modified.


This compiles in 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;
Bride answered 7/12, 2010 at 18:26 Comment(2)
the problem is in the implements clause. What you wrote is essentially the same as the TChild class OP wrote.Sandler
My answer was relevant to the original post before he edited it. In that example code he declared variables as IChild and got a compiler error when he tried to call DoSomething.Bride
L
-1

The Delphi implementation of QueryInterface is not up to standard. In the blog entry titled The ways people mess up IUnknown::QueryInterface Raymond Chen enumerates common failures in implementation. Most notable is the third point

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.

Unless an inherited interface is explicitly attached to a class or one of its ancestors Delphi fails to find it. It simply traverses the interface table of the object and its inherited types and checks for a match of the interface IDs, it does not check base interfaces.

Livelong answered 14/9, 2016 at 17:56 Comment(3)
This analysis is erroneous. Danny explained that Delphi was working around some oddities in MS code when they designed this. The implementation of QI is actually a combination of things. You implement it by specifying the interfaces you support and relying on the QI given in TInterfacedObject. Don't think of that function being the sole part of your implementation of QI. It relies on the interfaces that you declare support for. It is your job to list the ancestors.Circum
@DavidHeffernan I read and understood why they worked around the IClassFactory2 bug, but they also broke the concept that a class that implements derived interface implicitly implements its ancestor.Livelong
They aren't implementing QI. You are. By the interfaces you declare to be implemented.Circum

© 2022 - 2024 — McMap. All rights reserved.