Delphi接口实现

15

我认为接口实现中的引用计数应该在外部聚合对象上工作。如果可以提到另一个例子:Clarity in classes implementing multiple interfaces (alternative to delegation):

这是行为的最小复现:

program SO16210993;

{$APPTYPE CONSOLE}

type
  IFoo = interface
    procedure Foo;
  end;

  TFooImpl = class(TInterfacedObject, IFoo)
    procedure Foo;
  end;

  TContainer = class(TInterfacedObject, IFoo)
  private
    FFoo: IFoo;
  public
    constructor Create;
    destructor Destroy; override;
    property Foo: IFoo read FFoo implements IFoo;
  end;

procedure TFooImpl.Foo;
begin
  Writeln('TFooImpl.Foo called');
end;

constructor TContainer.Create;
begin
  inherited;
  FFoo := TFooImpl.Create;
end;

destructor TContainer.Destroy;
begin
  Writeln('TContainer.Destroy called');//this line never runs
  inherited;
end;

procedure Main;
var
  Foo : IFoo;
begin
  Foo := TContainer.Create;
  Foo.Foo;
end;

begin
  Main;
  Readln;
end.
如果我使用 implements 代替,在 TImplementor 类中实现接口,那么析构函数会运行。

5
我有什么遗漏吗?我不知道。但我们肯定是有的。你忘记包含代码了!需要完整的程序来演示行为。否则我们只能猜测。 - David Heffernan
1
你有一些额外的引用或引用循环。为 TFirstSecond._AddRef 和 TFirstSecond._Release 添加覆盖,并在那里设置断点,获取完整的引用列表并查看哪些引用未被清除。 - Arioch 'The
问题在于你的接口被委托了。不确定为什么会导致这种行为。 - David Heffernan
好问题。我冒昧地制作了一个非常简单的演示测试案例。 - David Heffernan
委托的概念是 Delphi 中一个很好的官方功能,但似乎并不起作用? - Gryffe
1个回答

16
这里发生的情况是你调用了TContainer.Create,创建了一个对象实例。但是你将该实例分配给接口引用,全局变量Foo。由于该变量类型为IFoo,接口代理意味着实现对象是TFooImpl的实例,而不是TContainer的实例。
因此,没有任何东西对TContainer的实例进行引用,其引用计数从未增加,因此它永远不会被销毁。
我认为没有非常简单的解决方法。你可能可以使用TAggregatedObject,但它可能无法解决你的问题。它将强制你声明TContainer.FFoo的类型为TFooImpl,我想你不想这样做。无论如何,以下是重构后的代码:
program SO16210993_TAggregatedObject;

{$APPTYPE CONSOLE}

type
  IFoo = interface
    procedure Foo;
  end;

  TFooImpl = class(TAggregatedObject, IFoo)
    procedure Foo;
  end;

  TContainer = class(TInterfacedObject, IFoo)
  private
    FFoo: TFooImpl;
    function GetFoo: IFoo;
  public
    destructor Destroy; override;
    property Foo: IFoo read GetFoo implements IFoo;
  end;

procedure TFooImpl.Foo;
begin
  Writeln('TFooImpl.Foo called');
end;

destructor TContainer.Destroy;
begin
  Writeln('TContainer.Destroy called');//this line does run
  FFoo.Free;
  inherited;
end;

function TContainer.GetFoo: IFoo;
begin
  if not Assigned(FFoo) then
    FFoo := TFooImpl.Create(Self);
  Result := FFoo;
end;

procedure Main;
var
  Foo : IFoo;
begin
  Foo := TContainer.Create;
  Foo.Foo;
end;

begin
  Main;
  Readln;
end.

documentation确实提到了这一点:

您用来实现委托接口的类应该派生自TAggregationObject。

最初,我找不到有关此TAggregationObject的任何文档。最后我意识到它实际上被命名为TAggregatedObject并已documented

TAggregatedObject提供了一个聚合内部对象的功能,通过实现IInterface方法来委托给控制IInterface。聚合对象是由多个接口对象组成的对象。每个对象都实现自己的行为和接口,但所有对象共享相同的引用计数,即控制器对象的引用计数。在容器模式中,控制器是容器对象。TAggregatedObject本身不支持任何接口。然而,与聚合一样,它确实实现了IInterface的方法,这些方法被从它继承的对象使用。因此,TAggregatedObject作为实现创建聚合对象的接口的类的基础。TAggregatedObject用作创建包含对象和连接对象的类的基础。使用TAggregatedObject确保对IInterface方法的调用委托给聚合的控制IInterface。控制IInterface在TAggregatedObject的构造函数中指定,并由Controller属性指示。此外,源代码注释中还有以下内容:
TAggregatedObject和TContainedObject是适合作为接口对象的基类,这些对象旨在被聚合或包含在外部控制对象中。当在外部对象类声明中使用“implements”语法来实现接口属性时,请使用这些类型来实现内部对象。由代表控制器的聚合对象实现的接口不应与控制器提供的其他接口有所区别。聚合对象不能维护自己的引用计数-它们必须与其控制器具有相同的生命周期。为了实现这一点,聚合对象将引用计数方法反映到控制器上。TAggregatedObject只是将QueryInterface调用反映到其控制器上。从这样的聚合对象中,可以获取控制器支持的任何接口,以及仅控制器支持的接口。这对于实现使用一个或多个内部对象来实现控制器类中声明的接口的控制器类非常有用。聚合促进了对象层次结构中的实现共享。大多数聚合对象都应该从TAggregatedObject继承,特别是与“implements”语法一起使用时。

@FabricioAraujo 我最终找到了文档。原来是有个笔误!! - David Heffernan
我被其他地方找到的无数例子误导了。引用计数必须在某个地方进行,这是完全有道理的。感谢您的回答。 - Gryffe
不客气。这对我来说是一次有趣的学习经历! - David Heffernan
Marcos Santos提出了一种看起来很优雅的解决方案,允许将接口实现用作常规对象和聚合对象链接。该解决方案通过从TInterfacedObject继承实现类,使其既可以作为独立对象使用,也可以作为从TAggregatedObject继承的装饰器与implements关键字一起使用。 - nunopicado

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