从接口获取子接口

时间:2014-04-03 14:04:34

标签: delphi interface

这是接口的一个特例,其中一个类实现了同一接口的多个版本,即。类似于以下内容

IBase = interface
   procedure Foo;
end;

ISub = interface (IBase)
   procedure Bar;
end;

ISpecialBase = interface (IBase) end;

ISpecialSub = interface (ISub) end;

TMyClass = class(TInterfacedObject, ISpecialBase, ISpecialSub)

   procedure SpecialFoo1;
   procedure SpecialFoo2;
   procedure SpecialBar;

   procedure ISpecialBase.Foo = SpecialFoo1;

   procedure ISpecialSub.Foo = SpecialFoo2;
   procedure ISpecialSub.Bar = SpecialBar;

   function GetTheRightOne(parameters) : IBase;

end;

...

function TMyClass.GetTheRightOne(parameters) : IBase;
begin
   if (something complex depending on parameters) then
      Result := ISpecialBase(Self)
   else Result := ISpecialSub(Self)
end;

当然,在实际案例中大约有十几个ISpecialXxxx。

非常需要只有一个实例,即。我想避免创建适配器或虚拟实例只是为了推迟ISpecialXxxx实现,因为前一个设计的唯一目的正是让一个实例处理许多出色的接口(即TMyClass的RefCount可以达到千分之一秒) )。

现在问题是GetTheRightOne()返回一个IBase,但在某些时候我想检查是否可以将IBase强制转换为ISub。

有没有办法用上面的申报表格来做?

一种方法是添加

function GetSub : ISub;

到IBase,但这确实使设计变得更重,因为它必须为每个ISpecialXxxx实现,并且对于ISpecialXxxx“继承”将是多余的,所以我正在寻找更优雅的解决方案(假设它存在)。

(我有其他“膨胀”解决方案,所以我真的想强调我正在寻找一个不膨胀的解决方案)

编辑:更多细节

  • GUID存在于原始代码中(但缺少的不是造成困难的原因)
  • 支持& QueryInterface不起作用,因为ISpecialXxx需要每个类有多个版本的接口,所以ISub没有列出explitly,因此找不到。但是,当使用适配器/虚拟类来推迟接口时(因为可以明确列出ISub)

edit2:如果你想要血腥细节

检查https://code.google.com/p/dwscript/source/browse/trunk/Source/dwsJSONConnector.pas(r2492),TdwsJSONConnectorType类和IJSONLow接口,目标是在IConnectorCall作为IConnectorCall传递时从中检测到IConnectorFastCall,从而能够调用LowFastCall而不是LowCall

检测必须在TConnectorCallExpr.AssignConnectorSym第294行进行,其中当前有一个QueryInterface。

请注意,QueryInterface适用于TdwsJSONIndexReadCall& TdwsJSONIndexWriteCall,因为它们实现了IConnectorCall&来自不同类的IConnectorFastCall&实例。但这就是我想避免的。

理想情况下,目标是将所有内容折回到ConnectorType类(单个类,单个实例)中,对于每个接口,特定的ConnectorType类应该可以自由地实现IConnectorCall或IConnectorFastCall。

4 个答案:

答案 0 :(得分:7)

要查看接口的实现者是否实现了另一个接口,您可以使用Supports或QueryInterface,如下面的伪代码所示:

var
  Base: IBase;
  Sub: ISub;
begin
  Base := X.GetTheRightOne(Params);  
  if Supports(Base, ISub, Sub) then
    Sub.Bar;
end;

编辑:要使上述工作正常,您需要将IID添加到接口的声明中。

答案 1 :(得分:6)

一种hackish方式依赖于编译器如何存储接口VTable数据。编译器为对象实现的每个接口存储单独的VTable。在每个VTable之后,它存储对象实现的接口数量。

因此我们可以使用它来确定我们是否获得了祖先接口的VTable,或者是后代的VTable。

至少这是它在XE3和XE5中的工作方式,我必须承认,在谈到接口的实现方式时,我有点像n00b。

除了依赖于实现细节之外,还有一个缺点是,如果向IBase接口添加方法,则必须保持GetSub功能同步。此外,如果您有两个不同的,不相关的ISub,则此代码无法检测到您获得的内容。你也许能够破解它,但我宁愿不去那里......

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  IBase = interface
    procedure Foo;
  end;

  ISub = interface (IBase)
    procedure Bar;
  end;

  ISpecialBase = interface (IBase)
  end;

  ISpecialSub = interface (ISub)
  end;

  TMyClass = class(TInterfacedObject, ISpecialBase, ISpecialSub)

    procedure SpecialFoo1;
    procedure SpecialFoo2;
    procedure SpecialBar;

    procedure ISpecialBase.Foo = SpecialFoo1;

    procedure ISpecialSub.Foo = SpecialFoo2;
    procedure ISpecialSub.Bar = SpecialBar;

    function GetTheRightOne(const Param: boolean) : IBase;
  end;


{ TMyClass }

function TMyClass.GetTheRightOne(const Param: boolean): IBase;
begin
  if Param then
    Result := ISpecialBase(Self)
  else
    Result := ISpecialSub(Self);
end;

procedure TMyClass.SpecialBar;
begin
  WriteLn('SubBar');
end;

procedure TMyClass.SpecialFoo1;
begin
  WriteLn('BaseFoo');
end;

procedure TMyClass.SpecialFoo2;
begin
  WriteLn('SubFoo');
end;

function GetSub(const Intf: IInterface): ISub;
type
  PPVtable = ^PVtable;
  PVtable = ^TVtable;
  TVtable = array[0..MaxInt div SizeOf(Pointer) - 1] of Pointer;
var
  intfVTable: PPVtable;
  caddr: NativeUInt;
begin
  result := nil;
  intfVTable := PPVTable(Intf);
  // 3 is offset to user methods
  // +0 = first user method, +1 = second user method etc
  // get the "address" of the first method in ISub
  caddr := NativeUInt(intfVTable^[3+1]);
  // compiler stores number of interface entries the
  // implementing object implements right after the interface vtable
  // so if we get a low number here, it means Intf is the IBase interface
  // and not the ISub
  if caddr > $100 then
    result := ISub(Intf);
end;

procedure CallIt(const b: IBase);
var
  s: ISub;
begin
  b.Foo;

  s := GetSub(b);
  if Assigned(s) then
    s.Bar;
end;

var
  c: TMyClass;
  b: IBase;
begin
  try
    c := TMyClass.Create;

    b := c.GetTheRightOne(True);
    CallIt(b);

    WriteLn('---');

    b := c.GetTheRightOne(False);
    CallIt(b);

    WriteLn('...');

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

此输出

BaseFoo
---
SubFoo
SubBar
...

我们想要的。

答案 2 :(得分:5)

这是我目前的“最佳”解决方案:

我放弃了方法解析子句,并转移到绑定到主类的虚拟类,并且只实例化了一次。

这样,GetInterface&可以使用支持,因为ISub再次显式。

但是,这引发了循环引用的问题:主类需要引用特殊内容(如果只是在GetTheRightOne()中返回它们),并且特殊内容需要引用主类(访问存储在那里的参数或重定向)到主类的方法)。

主类和特殊元素都是引用计数接口,当然,使用上下文是多线程的,因此通常的弱引用方案会引入对全局锁的需求。

然而,鉴于特殊内容是仅用于主类的接口解析的虚拟类,我们可以覆盖它们的_AddRef& _Release让引用计数集中在主类上(即_AddRef& _Release只是重定向到主类的_AddRef& _Release,并且不再保持自己的引用计数。)

答案 3 :(得分:3)

接口继承不遵循与类继承相同的原则。因此,为了测试IBase是否支持ISub,实现类需要显式声明ISub:

TMyClass = class(TInterfacedObject, ISub, ISpecialBase, ISpecialSub)

查询接口不会检查继承的接口。 AFAIR当在Delphi 2(?)中引入的接口时,其中一个编译器人曾经注意到接口继承只不过是语法糖。