2014-03-26 58 views
15

今天测试了一些东西时,我遇到了一个奇怪的情况。Delphi界面参考计数

我有一些接口和对象。代码如下所示:

IInterfaceZ = interface(IInterface) 
['{DA003999-ADA2-47ED-A1E0-2572A00B6D75}'] 
    procedure DoSomething; 
end; 

IInterfaceY = interface(IInterface) 
    ['{55BF8A92-FCE4-447D-B58B-26CD9B344EA7}'] 
    procedure DoNothing; 
end; 

TObjectB = class(TInterfacedObject, IInterfaceZ) 
    procedure DoSomething; 
end; 

TObjectC = class(TInterfacedObject, IInterfaceY) 
public 
    FTest: string; 
    procedure DoNothing; 
end; 

TObjectA = class(TInterfacedObject, IInterfaceZ, IInterfaceY) 
private 
    FInterfaceB: IInterfaceZ; 
    FObjectC: TObjectC; 
    function GetBB: IInterfaceZ; 
public 
    procedure AfterConstruction; override; 
    procedure BeforeDestruction; override; 
    property BB: IInterfaceZ read GetBB implements IInterfaceZ; 
    property CC: TObjectC read FObjectC implements IInterfaceY; 
end; 

procedure TObjectB.DoSomething; 
begin 
    Sleep(1000); 
end; 

procedure TObjectA.AfterConstruction; 
begin 
    inherited; 
    FInterfaceB := TObjectB.Create; 
    FObjectC := TObjectC.Create; 
    FObjectC.FTest := 'Testing'; 
end; 

procedure TObjectA.BeforeDestruction; 
begin 
    FreeAndNil(FObjectC); 
    FInterfaceB := nil; 
    inherited; 
end; 

function TObjectA.GetBB: IInterfaceZ; 
begin 
    Result := FInterfaceB; 
end; 

procedure TObjectC.DoNothing; 
begin 
    ShowMessage(FTest); 
end; 

现在,如果我访问各种实现这样我得到如下结果:

procedure TestInterfaces; 
var 
    AA: TObjectA; 
    YY: IInterfaceY; 
    ZZ: IInterfaceZ; 
    NewYY: IInterfaceY; 
begin 
    AA := TObjectA.Create; 
    // Make sure that the Supports doesn't kill the object. 
    // This line of code is necessary in XE2 but not in XE4 
    AA._AddRef; 

    // This will add one to the refcount for AA despite the fact 
    // that AA has delegated the implementation of IInterfaceY to 
    // to FObjectC. 
    Supports(AA, IInterfaceY, YY); 
    YY.DoNothing; 

    // This will add one to the refcount for FInterfaceB. 
    // This is also allowing a supports from a delegated interface 
    // to another delegated interface. 
    Supports(YY, IInterfaceZ, ZZ); 
    ZZ.DoSomething; 

    // This will fail because the underlying object is actually 
    // the object referenced by FInterfaceB. 
    Supports(ZZ, IInterfaceY, NewYY); 
    NewYY.DoNothing; 
end; 

第一个支持呼叫,它采用可变的工具,返回YY这实际上是对TObjectA的引用。我的AA变量是参考计数。因为底层引用计数对象是一个TObjectA,所以第二个支持使用支持调用中的接口,它工作并返回一个接口。底层对象实际上现在是一个TObjectB。 FInterfaceB后面的内部对象是被引用计数的对象。这部分是有意义的,因为GetBB实际上是FInterfaceB。正如预期的那样,最后一次对Supports的调用返回NewYY的空值,并且最后的调用失败。

我的问题是这个,引用第一个支持设计的支持电话是否引用TObjectA?换句话说,当实现接口的属性返回一个对象而不是一个接口时,这是否意味着所有者对象将成为引用计数的对象?我一直认为,实现也会导致内部委托对象被引用计数而不是主要对象。

的声明如下:

property BB: IInterfaceZ read GetBB implements IInterfaceZ; 

通过上述这个选项,后面FInterfaceB内部对象是一个是引用计数。

property CC: TObjectC read FObjectC implements IInterfaceY; 

利用上述第二种选择,TObjectA是正在引用计数,而不是委托对象FObjectC之一。

这是设计吗?

编辑

我只是在XE2测试这和行为是不同的。第二个支持声明返回零为ZZ。 XE4中的调试器告诉我YY指的是(TObjectA作为IInterfaceY)。在XE2中它告诉我它的一个(指针为IInterfaceY)。另外,在XE2中,AA不是在第一个支持语句中被引用,而是内部FObjectC是引用计数。

问题后附加信息回答

有一点需要注意这一点。您可以链接接口版本,但不链接对象版本。这意味着,像这样将工作:

TObjectBase = class(TInterfacedObject, IMyInterface) 
    … 
end; 

TObjectA = class(TInterfacedObject, IMyInterface) 
    FMyInterfaceBase: IMyInterface; 
    property MyDelegate: IMyInterface read GetMyInterface implements IMyInterface; 
end; 

function TObjectA.GetMyInterface: IMyInterface; 
begin 
    result := FMyInterfaceBase; 
end; 

TObjectB = class(TInterfacedObject, IMyInterface) 
    FMyInterfaceA: IMyInterface; 
    function GetMyInterface2: IMyInterface; 
    property MyDelegate2: IMyInterface read GetMyInterface2 implements IMyInterface; 
end; 

function TObjectB.GetMyInterface2: IMyInterface; 
begin 
    result := FMyInterfaceA; 
end; 

但对象版本给出了编译器错误此说TObjectB没有实现该接口的方法。

TObjectBase = class(TInterfacedObject, IMyInterface) 
    … 
end; 

TObjectA = class(TInterfacedObject, IMyInterface) 
    FMyObjectBase: TMyObjectBase; 
    property MyDelegate: TMyObjectBase read FMyObjectBase implements IMyInterface; 
end; 

TObjectB = class(TInterfacedObject, IMyInterface) 
    FMyObjectA: TObjectA; 
    property MyDelegate2: TObjectA read FMyObjectA implements IMyInterface; 
end; 

所以,如果你想开始链接委托,那么你需要坚持接口或以另一种方式解决它。

+0

你对TObjectA只有很弱的参考,因此你会失去AA,剩下的就是AA。 –

+0

@SirRufo我明白,这仅仅是一个例子。我更关心引用计数是如何完成的。基本上哪个对象被引用计数。 – Graymatter

+0

只需重写_AddRef/_Release方法调用并记录它们以及主程序,然后就可以看到它。 –

回答

17

TL;博士这是所有设计 - 它只是XE2以及XE3之间的设计变更。

XE3后来

有相当一个接口类型属性,并委托一类type属性代表团之间的差异。事实上,documentation明确地针对两个委派变体的不同部分调用了这种差异。

从你的角度的区别是如下:

  • TObjectA通过委托给类类型属性CC实现IInterfaceY,执行对象是TObjectA实例。
  • TObjectA通过委托给接口类型属性BB实现IInterfaceZ时,实现对象是实现了FInterfaceB的对象。

在这一切中实现的一个关键是,当您委托给类类型属性时,委派的类不需要实现任何接口。所以它不需要执行IInterface,因此不需要有_AddRef_Release方法。

看到这一点,修改TObjectC代码的定义是,像这样:

TObjectC = class 
public 
    procedure DoNothing; 
end; 

你会看到,这个代码编译,运行和行为完全一样的版本相同的方式。

实际上,这是理想的方式,您将如何声明将接口委派为类类型属性的类。这样做避免了混合接口和类类型变量的使用寿命问题。

那么,让我们看看你的三个电话到Supports

Supports(AA, IInterfaceY, YY); 

这里的实施对象为AA等的AA的引用计数递增。

Supports(YY, IInterfaceZ, ZZ); 

这里的实现对象是TObjectB的实例,所以它的引用计数是递增的。

Supports(ZZ, IInterfaceY, NewYY); 

这里,ZZ是不执行IInterfaceYTObjectB实例实现的接口。因此Supports返回FalseNewYYnil

XE2和更早

XE2 XE3和之间的设计变更与引进的移动ARM编译器的一致,并且有许多低层次的改变来支持ARC。很显然,这些更改中的一部分也适用于桌面编译器。

我可以找到的行为差异与类类型属性的接口实现委派有关。特别是当问题的类型支持IInterface时。在这种情况下,在XE2中,引用计数由内部对象执行。这与具有由外部对象执行的引用计数的XE3不同。

请注意,对于不支持IInterface的类类型,引用计数由所有版本中的外部对象执行。这是有道理的,因为内部对象无法做到这一点。

这里是我的示例代码演示的区别:

{$APPTYPE CONSOLE} 

uses 
    SysUtils; 

type 
    Intf1 = interface 
    ['{56FF4B9A-6296-4366-AF82-9901A5287BDC}'] 
    procedure Foo; 
    end; 

    Intf2 = interface 
    ['{71B0431C-DB83-49F0-B084-0095C535AFC3}'] 
    procedure Bar; 
    end; 

    TInnerClass1 = class(TObject, Intf1) 
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; 
    function _AddRef: Integer; stdcall; 
    function _Release: Integer; stdcall; 
    procedure Foo; 
    end; 

    TInnerClass2 = class 
    procedure Bar; 
    end; 

    TOuterClass = class(TObject, Intf1, Intf2) 
    private 
    FInnerObj1: TInnerClass1; 
    FInnerObj2: TInnerClass2; 
    public 
    constructor Create; 
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; 
    function _AddRef: Integer; stdcall; 
    function _Release: Integer; stdcall; 
    property InnerObj1: TInnerClass1 read FInnerObj1 implements Intf1; 
    property InnerObj2: TInnerClass2 read FInnerObj2 implements Intf2; 
    end; 

function TInnerClass1.QueryInterface(const IID: TGUID; out Obj): HResult; 
begin 
    if GetInterface(IID, Obj) then 
    Result := 0 
    else 
    Result := E_NOINTERFACE; 
end; 

function TInnerClass1._AddRef: Integer; 
begin 
    Writeln('TInnerClass1._AddRef'); 
    Result := -1; 
end; 

function TInnerClass1._Release: Integer; 
begin 
    Writeln('TInnerClass1._Release'); 
    Result := -1; 
end; 

procedure TInnerClass1.Foo; 
begin 
    Writeln('Foo'); 
end; 

procedure TInnerClass2.Bar; 
begin 
    Writeln('Bar'); 
end; 

constructor TOuterClass.Create; 
begin 
    inherited; 
    FInnerObj1 := TInnerClass1.Create; 
end; 

function TOuterClass.QueryInterface(const IID: TGUID; out Obj): HResult; 
begin 
    if GetInterface(IID, Obj) then 
    Result := 0 
    else 
    Result := E_NOINTERFACE; 
end; 

function TOuterClass._AddRef: Integer; 
begin 
    Writeln('TOuterClass._AddRef'); 
    Result := -1; 
end; 

function TOuterClass._Release: Integer; 
begin 
    Writeln('TOuterClass._Release'); 
    Result := -1; 
end; 

var 
    OuterObj: TOuterClass; 
    I1: Intf1; 
    I2: Intf2; 

begin 
    OuterObj := TOuterClass.Create; 

    Supports(OuterObj, Intf1, I1); 
    Supports(OuterObj, Intf2, I2); 

    I1.Foo; 
    I2.Bar; 

    I1 := nil; 
    I2 := nil; 

    Readln; 
end. 

上XE2输出:

 
TInnerClass1._AddRef 
TOuterClass._AddRef 
Foo 
Bar 
TInnerClass1._Release 
TOuterClass._Release 

上XE3输出:

 
TOuterClass._AddRef 
TOuterClass._AddRef 
Foo 
Bar 
TOuterClass._Release 
TOuterClass._Release 

讨论

为什么设计更改?我无法明确地回答这个问题,也没有参与决策。但是,XE3的行为对我来说感觉更好。如果你声明一个类类型变量,你会期望它的生命周期被管理为任何其他类类型变量。也就是说,通过显式调用桌面编译器上的析构函数,并通过移动编译器上的ARC进行调用。

另一方面,XE2的行为感觉不一致。为什么应该将一个属性用于接口实现委派的事实改变了它的生命周期管理方式?

所以,我的直觉告诉我,这是一个设计缺陷,至多在原始实现接口实现委托中。设计上的缺陷导致了多年来的混乱和终身管理问题。ARC的介绍迫使英巴卡迪诺审查这个问题,他们改变了设计。我相信ARC的引入需要设计更改,因为Embarcadero拥有不改变行为的记录,除非绝对必要。

以上段落显然是我的猜测,但这是我必须提供的最好的!

+0

+1,“...类型属性...不需要实现任何接口”。太好了,我可以在上午10:30之前学到新的东西 – iamjoosy

+0

@iamjoosy是的,这对我来说也是新的! –

+0

很好的答案。我同意XE3中的行为更好。它更一致。它还使开发人员可以选择他们希望进行引用计数的位置。在XE2及更早版本中,内部对象将成为接口背后的底层对象(当它实现接口时)。这限制了在使用委派时在接口上使用支持。 – Graymatter

3

你在混合对象指针和接口指针,这总是一个灾难的秘诀。 TObjectA不会递增其内部对象的引用计数以确保它们在整个生命周期内保持活动状态,并且TestInterfaces()不增加引用计数AA以确保其在整个测试集中存活。对象指针不参与引用计数!你必须手动管理它,例如:

procedure TObjectA.AfterConstruction; 
begin 
    inherited; 
    FObjectB := TObjectB.Create; 
    FObjectB._AddRef; 
    FObjectC := TObjectC.Create; 
    FObjectC._AddRef; 
    FObjectC.FTest := 'Testing'; 
end; 

procedure TObjectA.BeforeDestruction; 
begin 
    FObjectC._Release; 
    FObjectB._Release; 
    inherited; 
end; 

AA := TObjectA.Create; 
AA._AddRef; 

不用说,手工引用计数破坏了使用接口。

当与接口打交道,你需要:

  1. 禁用引用计数完全避免过早残害。例如,TComponent就是这样做的。

  2. 使用接口指针进行一切事务,从不使用对象指针。这确保了全面的参考计数。这通常是首选解决方案。

+0

这只是一小段测试代码。我没有表现出自己或类似的东西的错误。我更关心连接到接口的底层对象是什么,以及在两种情况下哪个对象是引用计数。我了解引用计数的问题。如果我看看你的AfterConstruction方法并参考我的原始示例。没有必要在FObjectC上做一个_AddRef,因为它永远不会被统计。这就是我的问题所在。在我添加的代码中,FObjectB被引用计数,但FObjectC不是。清理问题。 – Graymatter

+0

XE2在此代码中表现出与XE4不同的行为。我已经更新了这个问题以显示不同之处。 – Graymatter

+0

@Remy尽管你说的都是正确的,但我不认为你真的理解了这个问题。 –