处理Delphi中的循环强引用

Dee*_*031 6 delphi interface reference-counting delphi-10.1-berlin

我有两个类(在我的示例中为TObject1和TObject2),它们通过接口(IObject1,IObject2)相互了解.正如您在Delphi中可能知道的那样,这将导致内存泄漏,因为参考计数器将始终保持在零以上.通常的解决方案是将一个引用声明为弱.这在大多数情况下都适用,因为你通常知道哪一个会被破坏,或者一旦它被销毁就不一定需要弱引用后面的对象.

这说我尝试以这样的方式解决问题,即两个对象都保持活着,直到两者都不再被引用:(因为我使用[unsafe]属性需要Delphi 10.1)

program Project14;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  IObject2 = interface;

  IObject1 = interface
    ['{F68D7631-4838-4E15-871A-BD2EAF16CC49}']
    function GetObject2: IObject2;
  end;

  IObject2 = interface
    ['{98EB60DA-646D-4ECF-B5A7-6A27B3106689}']
  end;

  TObject1 = class(TInterfacedObject, IObject1)
    [unsafe] FObj2: IObject2;
    constructor Create;
    destructor Destroy; override;

    function GetObject2: IObject2;
  end;

  TObject2 = class(TContainedObject, IObject2)
    [unsafe] FObj1: IObject1;
    constructor Create(aObj1: IObject1);
    destructor Destroy; override;
  end;

constructor TObject1.Create;
begin
  FObj2 := TObject2.Create(Self);
end;

destructor TObject1.Destroy;
begin
  TContainedObject(FObj2).Free;
  inherited Destroy;
end;

function TObject1.GetObject2: IObject2;
begin
  Result := FObj2;
end;

constructor TObject2.Create(aObj1: IObject1);
begin
  inherited Create(aObj1);
  FObj1 := aObj1;
end;

destructor TObject2.Destroy;
begin
  inherited Destroy;
end;

function Test1: IObject1;
var
  x: IObject2;
begin
  Result := TObject1.Create;
  x := Result.GetObject2;
end;

function Test2: IObject2;
var
  x: IObject1;
begin
  x := TObject1.Create;
  Result := x.GetObject2;
end;

var
  o1: IObject1;
  o2: IObject2;
begin
  try
    o1 := Test1();
    o2 := Test2();
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Run Code Online (Sandbox Code Playgroud)

这确实有效..函数Test1和Test2每个创建一个TObject1和TObject2的实例互相引用,一旦o1和o2超出范围,所有实例都会被销毁.该解决方案基于TContainedObject,它将引用计数转发给"控制器"(在本例中为TObject1).

现在我知道这个解决方案存在缺陷,这就是我的问题所在:

  • "TContainedObject(FObj2).自由;" 闻起来有点,但我没有更好的解决方案,因为我需要使用一个接口来引用TObject2(生产代码在这一端包含一些继承).有什么想法来清理它吗?
  • 你很容易忘记将两个类之间的所有引用声明为弱和...
  • 一个类似的问题开始引发更多类:拥有一个引用的TObject3并引用另一个:内存泄漏.我可以通过让它从TContainedObject下降来处理它,但是使用遗留代码这可能不是一件容易的事.

我觉得这个解决方案不能普遍应用,并且希望有一个可以 - 或者可能是一个答案,它将描述为什么它很难甚至不可能有一个易于使用的100%解决方案来管理这种情况.Imho,如果没有从域中引用它们而不必仔细考虑该域中的每个引用,那么有一定数量的对象会相互破坏并不复杂.

Joh*_*ica 1

不要使用不安全
[unsafe]不应该在正常代码中使用。
如果您不希望编译器在接口上进行引用计数,那么这对用户来说确实是一种黑客攻击。

使用弱代替
如果由于某种原因您必须有循环引用,则[weak]在其中一个引用上使用属性并像往常一样声明另一个引用。

在你的例子中它看起来像这样:

  TParent = class(TInterfacedObject, IParent)
    FChild: IChild;   //normal child
    constructor Create;
    function GetObject2: IChild;
  end;

  TChild = class(TContainedObject, IChild)
    //reference from the child to the parent, always [weak] if circular.
    [weak] FObj1: IParent;   
    constructor Create(const aObj1: IParent);
  end;
Run Code Online (Sandbox Code Playgroud)

现在不需要在析构函数中做任何特殊的事情,因此可以省略这些。
编译器跟踪所有弱引用,并在引用接口的引用计数达到零时将它们设置为 nil。
而这一切都是以线程安全的方式完成的。
然而弱引用本身并不会增加引用计数。

何时使用不安全
这与不安全引用相反,不安全引用根本不发生任何跟踪和引用计数。

[unsafe]您可以在单例接口类型或禁用引用计数的接口类型上使用引用。
这里引用计数在任何情况下都固定为-1,因此调用addref和release是不必要的开销。
消除了[unsafe]这种愚蠢的开销。
除非您的接口覆盖_addref并且_release不使用[unsafe].

Pre Berlin 替代方案Pre Berlin在 NexGen 编译器之外
没有任何属性。 如果您运行的是 Seattle, 2010 或介于两者之间的任何内容,则以下代码将执行{几乎}相同的操作。尽管我不确定这段代码是否会成为多线程代码中竞争条件的受害者。 如果您担心这个问题,请随时举旗,我会进行调查。[weak]


  TParent = class(TInterfacedObject, IParent)
    FChild: IChild;   //normal child
    constructor Create;
    function GetObject2: IChild;
  end;

  TChild = class(TContainedObject, IChild)
    //reference from the child to the parent, always [weak] if circular.
    FObj1: TParent;   //not an interface will not get refcounted.  
    constructor Create(const aObj1: IParent);
    destructor Destroy; override;
  end;

  constructor TChild.Create(const aObj1: IParent);
  begin
    inherited Create;
    FObject1:= (aObj1 as TParent);
  end;

 destructor TParent.Destroy;
 begin
   if Assigned(FChild) then FChild.InvalidateYourPointersToParent(self);
   inherited;
 end;
Run Code Online (Sandbox Code Playgroud)

这也将确保接口得到正确处理,但现在TChild.FObject1不会自动被清空。您可以将代码放入 的析构函数中TParent以访问其所有子级并通知它们,如所示的代码所示。
如果循环引用中的参与者之一无法通知其弱链接的对应方,那么您将需要设置一些其他机制来消除这些弱引用。