【问题标题】:Dealing with circular strong references in Delphi在 Delphi 中处理循环强引用
【发布时间】:2016-09-04 07:30:25
【问题描述】:

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

这表示我试图以两种对象都保持活动状态直到不再引用它们的方式来解决问题:(需要 Delphi 10.1,因为我使用 [unsafe] 属性)

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.

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

现在我知道这个解决方案有缺陷,这就是我的问题开始的地方:

  • “TContainedObject(FObj2).Free;”有点味道,但我没有更好的解决方案,因为我需要使用一个接口来引用 TObject2(生产代码为此包含一些继承)。有什么办法可以清理吗?
  • 您很容易忘记将 2 个类之间的所有引用声明为 weak 和 ..
  • 随着类的增多,类似的问题开始出现:TObject3 被一个引用并引用另一个:内存泄漏。我也可以让它从 TContainedObject 下降来处理它,但是对于遗留代码,这可能不是一件容易的事。

我觉得这个解决方案不能被普遍应用,并希望有一个可以 - 或者可能会描述为什么很难甚至不可能有一个易于使用的 100% 解决方案来管理这样的解决方案情况。 恕我直言,拥有有限数量的对象并不会太复杂,一旦它们没有从其域外被引用,它们就会相互破坏,而不必仔细考虑该域内的每个引用。

【问题讨论】:

  • 如果对象 1 创建了对象 2,这向我强烈暗示了对象 1 负责并依赖对象 2 的关系。为什么这不是强引用?您提到这不是“真实代码”,我认为真正的问题在于该真实代码中的协作设计,而不是这种枯燥的技术场景。如果您发布了您实际合作的详细信息,也许可以就更合适/可行的方法提出一些建议?
  • 您应该几乎总是避免循环引用。这是几乎可以普遍应用的解决方案。你为什么要找一个做坏事的好方法?关于可测试性,然后问为什么需要两个类,其中 A 引用 B,B 引用 A。为什么这是个好主意?如果你可以重构为 A、B 和 C,其中 C 管理 A 和 B,而 A 和 B 都不知道 C。
  • 不要使用[unsafe],而是使用[weak],哦,[unsafe][weak] 已经在下一代编译器中使用了很长一段时间了。只有 windows 和 osx 编译器在 10.1 中获得了它们。
  • @WarrenP 你可能是对的,在我的例子中,“TObject1”有很多责任,这会导致循环引用。拥有处理这种情况的工具仍然会很好。

标签: delphi interface reference-counting delphi-10.1-berlin


【解决方案1】:

您似乎希望两个对象共享它们的引用计数。您可以通过让第三个对象 (TPair) 处理引用计数来做到这一点。一个很好的方法是使用implements 关键字。您可以选择隐藏这第三个对象,或与之交互。

使用下面的代码,您可以创建TPairChildATPairChildB 或它们的“父级”TPair。它们中的任何一个都会在需要时创建其他对象,并且所有创建的对象都将保持活动状态,直到不再引用任何对象。您当然可以将像 IObject1 这样的接口添加到对象中,但为了简单起见,我将它们保留在外面。

unit ObjectPair;

interface

type
  TPairChildA = class;
  TPairChildB = class;

  TPair = class( TInterfacedObject )
  protected
    FChildA : TPairChildA;
    FChildB : TPairChildB;

    function GetChildA : TPairChildA;
    function GetChildB : TPairChildB;
  public
    destructor Destroy; override;

    property ChildA : TPairChildA read GetChildA;
    property ChildB : TPairChildB read GetChildB;
  end;

  TPairChild = class( TObject , IInterface )
  protected
    FPair : TPair;

    property Pair : TPair read FPair implements IInterface;
  public
    constructor Create( APair : TPair = nil ); virtual;
  end;

  TPairChildA = class( TPairChild )
  protected
    function GetSibling : TPairChildB;
  public
    constructor Create( APair : TPair = nil ); override;

    property Sibling : TPairChildB read GetSibling;
  end;

  TPairChildB = class( TPairChild )
  protected
    function GetSibling : TPairChildA;
  public
    constructor Create( APair : TPair = nil ); override;

    property Sibling : TPairChildA read GetSibling;
  end;

implementation

//==============================================================================
// TPair

destructor TPair.Destroy;
begin
  FChildA.Free;
  FChildB.Free;
  inherited;
end;

function TPair.GetChildA : TPairChildA;
begin
  if FChildA = nil then
    FChildA := TPairChildA.Create( Self );
  Result := FChildA;
end;

function TPair.GetChildB : TPairChildB;
begin
  if FChildB = nil then
    FChildB := TPairChildB.Create( Self );
  Result := FChildB;
end;

// END TPair
//==============================================================================
// TPairChild

constructor TPairChild.Create( APair : TPair = nil );
begin
  if APair = nil then
    FPair := TPair.Create
  else
    FPair := APair;
end;

// END TPairChild
//==============================================================================
// TPairChildA

constructor TPairChildA.Create( APair : TPair = nil );
begin
  inherited;
  FPair.FChildA := Self;
end;

function TPairChildA.GetSibling : TPairChildB;
begin
  Result := FPair.ChildB;
end;

// END TPairChildA
//==============================================================================
// TPairChildB

constructor TPairChildB.Create( APair : TPair = nil );
begin
  inherited;
  FPair.FChildB := Self;
end;

function TPairChildB.GetSibling : TPairChildA;
begin
  Result := FPair.ChildA;
end;

// END TPairChildB
//==============================================================================

end.

使用示例:

procedure TForm1.Button1Click( Sender : TObject );
var
  objA : TPairChildA;
  ifA , ifB : IInterface;
begin
  objA := TPairChildA.Create;
  ifA := objA;
  ifB := objA.Sibling;
  ifA := nil;
  ifB := nil; // This frees all three objects.
end;

【讨论】:

    【解决方案2】:

    你在这里解决了错误的问题。

    您的实际问题不在于强弱引用,也不在于如何改进您的解决方案。你的问题不在于如何实现,而在于你正在实现什么(想要实现)

    首先直接解决您的问题:

    • “TContainedObject(FObj2).Free;”有点味道,但我没有更好的解决方案,因为我需要使用接口来引用 TObject2 (生产代码为此包含一些继承)。任何 清理它的想法?

    你不能在这里做很多事情。您必须在 FObj2 上调用 Free,因为 TContainedObject 本身不是托管类。

    • 您很容易忘记将 2 个类之间的所有引用声明为 weak 和 ..

    你也不能在这里做任何事情。它随领土而来。如果你想使用 ARC,你必须考虑循环引用。

    • 随着类的增多,类似的问题开始出现:让 TObject3 被一个引用并引用另一个:内存 泄漏。我可以通过让它从 TContainedObject 下降来处理它 也是如此,但对于遗留代码,这可能不是一件容易的事。

    你也不能在这里做太多事情。如果您的设计真的是您想要的,那么您只需要处理它的复杂性。


    现在,回到您最初遇到问题的原因。

    您想要实现的(并且您已经使用示例代码做到了)是通过抓取该层次结构中的任何对象引用来保持整个对象层次结构的活力。

    换种说法,你有FormButton,你想让Form活着,就是拥有Button(因为Button本身不起作用)。然后你想将Edit 添加到Form 中,如果有东西抓住Edit,则再次保持一切正常。

    您在这里几乎没有选择。

    • 保留这种损坏的设计并使用您的解决方案,因为您涉及的代码太多,更改会很痛苦。如果你这样做了,请记住这最终会破坏设计,不要试图在其他任何地方重复它。

    • 1234563 .相反,如果你真的需要,请获取 root TObject1 实例并传递它。
    • 这是第二种方法的变体。如果TObject1 不是根类,则创建包含您需要的所有实例的附加包装类并传递该类。

    最后两个解决方案远非完美,它们没有处理您可能有做太多或类似的类的事实。但无论该代码多么糟糕,它甚至都无法接近您当前的解决方案。随着时间的推移,您可以慢慢更改和改进这些解决方案,这比使用当前解决方案要容易得多。

    【讨论】:

      【解决方案3】:

      如果你想让两个对象同时存活或死亡,那么它们肯定是一个对象。好的,我知道两者可能是由不同的人开发的,所以我会让它们成为一个引用计数的超级对象的成员,就像这样

      type
        TSuperobject = class( TInterfaceObject, IObject1, iObject2 )
        private
          fObject1 : TObject1;
          fObject2 : TObject2;
        public
          constructor Create;
          destructor Destroy;
          function GetObject2: IObject2;
          etc.
        end;
      
      etc.
      

      细节应该是显而易见的。对 object1 或 object2 的任何引用都必须引用拥有对象( superobject.object1 等),因此 object1 和 object2 本身不需要进行引用计数 - 即它们可以是常规对象,而不是接口对象,但实际上并不重要如果它们被引用计数,因为所有者总是将引用计数加 1(在这种情况下,您可能不需要超级对象中的析构函数)。如果您将 object1 和 object2 保留为引用对象,则它们对彼此的引用都很弱。

      【讨论】:

        【解决方案4】:

        不要使用 unsafe
        [unsafe] 不应在普通代码中使用。
        如果您不希望编译器在接口上进行引用计数,那么这确实是一种 hack。

        改用弱
        如果由于某种原因您必须具有循环引用,则在其中一个引用上使用 [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;
        

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

        何时使用不安全
        这与不安全引用形成对比,不安全引用根本不进行跟踪和引用计数。

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

        柏林前替代方案
        Pre Berlin 在 NexGen 编译器之外没有 [weak] 属性。
        如果您运行的是 2010 年的西雅图或介于以下代码之间的任何代码,{几乎} 会做同样的事情。
        虽然我不确定这段代码是否可能不会成为多线程代码中竞争条件的牺牲品。
        如果您对此感到担忧,请随时举起旗帜,我会进行调查。

          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;
        

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

        【讨论】:

        • 这并不能回答我的问题,无论我使用 [unsafe]、[weak] 还是任何其他方式来避免引用计数并不重要,因为我的代码的目的是让两个对象保持活动状态,直到两者都不再被引用,因此它们一起被销毁。请注意,源代码只是为了解决我的实际问题,即只要至少有一个从外部引用,两个对象就保持活动状态。
        猜你喜欢
        • 2015-11-08
        • 2014-12-27
        • 1970-01-01
        • 1970-01-01
        • 2014-04-13
        • 2011-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多