gpt4 book ai didi

Delphi 接口(interface)引用计数

转载 作者:行者123 更新时间:2023-12-03 14:36:22 26 4
gpt4 key购买 nike

我今天测试时遇到了一个奇怪的情况。

我有许多接口(interface)和对象。代码如下所示:

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;

第一个 Supports 调用使用了实现中的变量,返回 YY,它实际上是对 TObjectA 的引用。我的 AA 变量是引用计数。因为底层引用计数对象是一个 TObjectA,第二个支持,它使用支持调用中的接口(interface),工作并返回一个接口(interface)。底层对象现在实际上是一个 TObjectB。 FInterfaceB 后面的内部对象是被引用计数的对象。这部分是有道理的,因为 GetBB 实际上是 FInterfaceB。正如此处预期的那样,对 Supports 的最后一次调用为 NewYY 返回 null,并且最后的调用失败。

我的问题是,TObjectA 上的引用计数是第一个支持设计调用的吗?换句话说,当实现接口(interface)的属性返回一个对象而不是一个接口(interface)时,这是否意味着所有者对象将是进行引用计数的对象?我总是觉得实现也会导致内部委托(delegate)对象被引用计数而不是主对象。

声明如下:
  property BB: IInterfaceZ read GetBB implements IInterfaceZ;

使用上面的这个选项,FInterfaceB 后面的内部对象是引用计数的对象。
  property CC: TObjectC read FObjectC implements IInterfaceY;

使用上面的第二个选项,TObjectA 是被引用计数的对象,而不是委托(delegate)对象 FObjectC。

这是故意的吗?

编辑

我刚刚在 XE2 中对此进行了测试,行为有所不同。第二个 Supports 语句为 ZZ 返回 nil。 XE4 中的调试器告诉我 YY 指的是 (TObjectA as IInterfaceY)。在 XE2 中,它告诉我它是一个(指针为 IInterfaceY)。此外,在 XE2 中,第一个支持语句中的 AA 不是 ref 计数的,而是内部 FObjectC 的引用计数。

问题回答后的附加信息

对此有一个警告。您可以链接接口(interface)版本,但不能链接对象版本。这意味着这样的事情会起作用:
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 没有实现接口(interface)的方法。
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;

因此,如果您想开始链接委托(delegate),那么您需要坚持使用接口(interface)或以另一种方式解决它。

最佳答案

tl;博士 这完全是设计 - 只是 XE2 和 XE3 之间的设计发生了变化。

XE3 及更高版本

对接口(interface)类型属性的委托(delegate)和对类类型属性的委托(delegate)有很大的不同。确实是documentation使用两个委托(delegate)变体的不同部分明确指出这种差异。

从你的角度来看,不同之处如下:

  • TObjectA工具IInterfaceY通过委托(delegate)给类类型属性 CC ,实现对象是TObjectA的实例.
  • TObjectA工具IInterfaceZ通过委托(delegate)给接口(interface)类型属性 BB ,实现对象是实现FInterfaceB的对象.

  • 在这一切中要实现的一个关键是,当您委托(delegate)给一个类类型属性时,被委托(delegate)的类不需要实现任何接口(interface)。所以它不需要实现 IInterface所以不需要 _AddRef_Release方法。

    要看到这一点,请修改您的代码定义 TObjectC像这样:
    TObjectC = class
    public
    procedure DoNothing;
    end;

    您将看到此代码的编译、运行和行为方式与您的版本完全相同。

    事实上,这就是理想情况下如何声明一个接口(interface)被委托(delegate)为类类型属性的类。这样做可以避免混合接口(interface)和类类型变量的生命周期问题。

    那么,让我们看看您对 Supports 的三个调用:
    Supports(AA, IInterfaceY, YY);

    这里的实现对象是 AA所以 AA 的引用计数递增。
    Supports(YY, IInterfaceZ, ZZ);

    这里的实现对象是 TObjectB的实例所以它的引用计数增加。
    Supports(ZZ, IInterfaceY, NewYY);

    在这里, ZZTObjectB的实例实现的接口(interface)没有实现 IInterfaceY .因此 Supports返回 FalseNewYYnil .

    XE2 及更早版本

    XE2 和 XE3 之间的设计更改与移动 ARM 编译器的引入相吻合,并且有许多低级更改以支持 ARC。显然,其中一些更改也适用于桌面编译器。

    我发现的行为差异涉及将接口(interface)实现委托(delegate)给类类型属性。特别是当所讨论的类类型支持 IInterface 时.在这种情况下,在 XE2 中,引用计数由内部对象执行。这与 XE3 不同,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

    酒吧
    TInnerClass1._Release
    TOuterClass._Release

    XE3 上的输出是:

    TOuterClass._AddRef
    TOuterClass._AddRef

    酒吧
    TOuterClass._Release
    TOuterClass._Release

    讨论

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

    另一方面,XE2 的行为让人感觉不一致。为什么将属性用于接口(interface)实现委托(delegate)这一事实会改变其生命周期的管理方式?

    所以,我的直觉告诉我,这充其量只是接口(interface)实现委托(delegate)的原始实现中的一个设计缺陷。多年来,设计缺陷导致了困惑和生命周期管理问题。 ARC 的引入迫使 Embarcadero 重新审视这个问题,他们改变了设计。我认为 ARC 的引入需要进行设计更改,因为 Embarcadero 有不改变行为的记录,除非绝对必要。

    上面的段落显然是我的推测,但这是我所能提供的最好的!

    关于Delphi 接口(interface)引用计数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22649433/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com