【问题标题】:Why is list of type TObjectList freed automatically after iteration?为什么迭代后会自动释放 TObjectList 类型的列表?
【发布时间】:2016-07-02 17:00:26
【问题描述】:

我对 Spring4D 框架的 TObjectList 类的行为有疑问。在我的代码中,我创建了一个几何图形列表,例如squarecircletriange,每个都定义为一个单独的类。为了在列表被破坏时自动释放几何图形,我定义了一个 TObjectList 类型的列表,如下所示:

procedure TForm1.FormCreate(Sender: TObject);
var
  geometricFigures: TObjectList<TGeometricFigure>;
  geometricFigure: TGeometricFigure;
begin
  ReportMemoryLeaksOnShutdown := true;

  geometricFigures := TObjectList<TGeometricFigure>.Create();
  try
    geometricFigures.Add(TCircle.Create(4,2));
    geometricFigures.Add(TCircle.Create(0,4));
    geometricFigures.Add(TRectangle.Create(3,10,4));
    geometricFigures.Add(TSquare.Create(1,5));
    geometricFigures.Add(TTriangle.Create(5,7,4));
    geometricFigures.Add(TTriangle.Create(2,6,3));

    for geometricFigure in geometricFigures do begin
      geometricFigure.ToString();
    end;
  finally
    //geometricFigures.Free(); -> this line is not required (?)
  end;
end;

如果我运行此代码,则列表 geometricFigures 会自动从内存中释放,即使我没有在列表中调用方法 Free(注意在 finally 块中注释掉的行)。我预计会有不同的行为,我认为该列表需要显式调用 Free() 因为局部变量 geometricFigures 未使用接口类型。

我进一步注意到,如果列表的项目没有在 for-in 循环中迭代(我暂时从代码中删除了它),则列表不会自动释放并且我会出现内存泄漏。

这使我想到以下问题: 为什么 TObjectList (geometricFigures) 类型的列表在其项被迭代时会自动释放,但如果从代码中删除 for-in 循环则不会?

更新

我听从了 Sebastian 的建议,调试了析构函数。列表项被以下代码破坏:

{$REGION 'TList<T>.TEnumerator'}

constructor TList<T>.TEnumerator.Create(const list: TList<T>);
begin
  inherited Create;
  fList := list;
  fList._AddRef;
  fVersion := fList.fVersion;
end;

destructor TList<T>.TEnumerator.Destroy;
begin
  fList._Release;
  inherited Destroy; // items get destroyed here
end;

更新

我不得不重新考虑我接受的答案并得出以下结论:

在我看来,Rudy 的回答是正确的,即使所描述的行为可能不是框架中的错误。我认为 Rudy 提出了一个很好的论点,他指出框架应该按预期工作。当我使用 for-in 循环时,我希望它是一个只读操作。之后清除列表不是我所期望的。

另一方面,Fritzw 和 David Heffernan 指出 Spring4D 框架的设计是基于接口的,因此应该以这种方式使用。只要记录了这种行为(也许 Fritzw 可以给我们参考文档),我同意 David 的观点,即我对框架的使用是不正确的,即使我仍然认为框架的行为具有误导性。

我在使用 Delphi 进行开发方面没有足够的经验来评估所描述的行为是否实际上是一个错误,因此撤销了我接受的答案,对此感到抱歉。

【问题讨论】:

    标签: delphi spring4d


    【解决方案1】:

    要使用for ... do 进行迭代,该类必须具有GetEnumerator 方法。这显然将自身(即TObjectList&lt;&gt;)作为IEnumerator&lt;TGeometricFigure&gt; 接口返回。迭代后,IEnumerator&lt;&gt;被释放,其引用计数为0,objectlist被释放。

    这是你经常在 C# 中看到的模式,但在那里,它没有这种效果,因为类实例仍然被引用,垃圾收集器不会跳进去。

    但是,正如您所见,在 Delphi 中,这是一个问题。我想解决方案是让TObjectList&lt;&gt; 拥有一个单独的(可能是嵌套的)类或记录来进行枚举,而不是返回Self(如IEnumerator&lt;&gt;)。但这取决于 Spring4D 的作者。您可以将这个问题提请 Stefan Glienke 的注意。

    更新

    您的附录表明这并不是真正发生的事情。 TObjectList&lt;&gt;(或者更准确地说,它的祖先 TList&lt;&gt;)返回一个单独的枚举器,但是这样做(IMO 完全没有必要,即使从一开始就将列表用作接口)_AddRef/_Release而后者是罪魁祸首。

    注意

    我看到多个声明在 Spring4D 中,该类不应用作类。那么这样的类不应该在interface 部分中公开,而是在单元的implementation 部分中公开。如果这些类被公开,作者应该期望用户使用它们。如果它们可以用作类,那么 for-in 循环不应该释放容器。其中之一是设计问题:要么作为类曝光,要么自动释放。所以有一个错误,IMO。

    【讨论】:

    • Spring4d 旨在使用接口而不是类。它不是设计和记录的错误
    • 问题中的代码存在缺陷,框架很好,行为符合设计
    • 对不起,我不同意。 _AddRef 和 _Release 是原因,这不应该发生。容器上的 for-in 永远不会触发释放容器,因此显然框架的行为与预期不同(无论它是否按设计运行)。它违反了最小惊讶的原则,如果不是更多的话。
    • @David - 设计中的缺陷仍然是缺陷。代码(在框架中)具有与设计中的缺陷一致的实现这一事实并不能改变它是错误的事实。这是软件质量 101 V&V - 验证(根据规范)只是适应性测试的一半。另一半是验证,说明规范首先是正确的。只有当设计本身被验证并确认为正确时,“按设计”才是有效的响应。在这种情况下,显然不是。
    • @Deltics TInterfacedObject 具有相同的行为。公开曝光。你还能如何继承它。如果你认为图书馆设计可以阻止消费者犯错,那你就太天真了。
    【解决方案2】:

    要了解为什么释放列表,我们需要了解幕后发生的事情。

    TObjectList&lt;T&gt; 旨在用作接口并具有引用计数。每当 refcount 达到 0 时,实例就会被释放。

    procedure foo;
    var
      olist: TObjectList<TFoo>;
      o: TFoo;
    begin
      olist := TObjectList<TFoo>.Create();
    

    olist 的引用计数现在为 0

      try
        olist.Add( TFoo.Create() );
        olist.Add( TFoo.Create() );
    
        for o in olist do 
    

    枚举器将olist的引用计数增加到1

        begin
          o.ToString();
        end;
    

    枚举器超出范围并调用枚举器的析构函数,这会将olist 的引用计数减少到0,这意味着olist 实例已被释放。

      finally
        //olist.Free(); -> this line is not required (?)
      end;
    end;
    

    使用接口变量有什么区别?

    procedure foo;
    var
      olist: TObjectList<TFoo>;
      olisti: IList<TFoo>;
      o: TFoo;
    begin
      olist := TObjectList<TFoo>.Create();
    

    olist 引用计数为 0

      olisti := olist;
    

    olist 引用分配给接口变量olisti 将在olist 上内部调用_AddRef 并将引用计数增加到1。

      try
        olist.Add( TFoo.Create() );
        olist.Add( TFoo.Create() );
    
        for o in olist do 
    

    枚举器将olist的引用计数增加到2

        begin
          o.ToString();
        end;
    

    枚举器超出作用域并调用枚举器的析构函数,这会将olist的引用计数减少到1。

      finally
        //olist.Free(); -> this line is not required (?)
      end;
    end;
    

    在过程结束时,接口变量olisti 将被设置为nil,这将在olist 上内部调用_Release 并将引用计数减少到0,这意味着olist 实例是释放。

    当我们将构造函数的引用直接分配给接口变量时,也会发生同样的情况:

    procedure foo;
    var
      olist: IList<TFoo>;
      o: TFoo;
    begin
      olist := TObjectList<TFoo>.Create();
    

    分配对接口变量olist的引用将在内部调用_AddRef并将引用计数增加到1。

      olist.Add( TFoo.Create() );
      olist.Add( TFoo.Create() );
    
      for o in olist do 
    

    枚举器将olist的引用计数增加到2

      begin
        o.ToString();
      end;
    

    枚举器超出范围,调用枚举器的析构函数,这会将olist的引用计数减少到1。

    end;
    

    在过程结束时,接口变量olist 将被设置为nil,这将在olist 上内部调用_Release 并将引用计数减少到0,这意味着olist 实例是释放。

    【讨论】:

      【解决方案3】:

      您正在使用for in loop 来遍历集合;这种循环在名为GetEnumerator 的类中查找方法。在Spring4D中,对于TObjectList&lt;T&gt;,你最终调用继承的TList&lt;T&gt;.GetEnumerator,实现为:

      function TList<T>.GetEnumerator: IEnumerator<T>;
      begin
        Result := TEnumerator.Create(Self);
      end;
      

      TEnumerator 的构造函数实现为:

      constructor TList<T>.TEnumerator.Create(const list: TList<T>);
      begin
        inherited Create;
        fList := list;
        fList._AddRef;
        fVersion := fList.fVersion;
      end;
      

      注意它会调用列表中的_AddRef。此时,您的 TObjetList RefCount 变为 1

      由于GetEnumerator 调用返回一个接口,当您完成循环时,它将被释放。 Destructor 是这样实现的:

      destructor TList<T>.TEnumerator.Destroy;
      begin
        fList._Release;
        inherited Destroy;
      end;
      

      注意它调用列表中的_Release。如果您开始使用调试器,您会注意到它将列表的RefCount 递减为0,然后调用_Release,这就是您的列表被释放的原因

      如果您在原始代码中删除 for in 循环,最终会导致内存泄漏:


      意外的内存泄漏

      发生了意外的内存泄漏。意外的小块泄漏是:

      1 - 12 字节:TGeometricFigure x 6、TMoveArrayManager x 1、未知 x 1

      21 - 28 字节:TList x 1

      29 - 36 字节:TCriticalSection x 1

      53 - 60 字节:TCollectionChangedEventImpl x 1,未知 x 1

      77 - 84 字节:TObjectList x 1

      编辑:刚刚看到 Rudy Velthuis 的回答。这不是 Spring4D 错误。您应该使用基于框架类的集合。您必须使用基于界面的集合。另外,与 Spring4D 无关,但在 Delphi 中,建议您不要将接口引用与对象引用混合

      【讨论】:

      • 枚举器不释放容器。枚举器在收到容器时会增加 RefCount,当它离开时会再次减少它。枚举器期望容器正确初始化:不接收 RefCount 为零的接口对象。另外,如果您将类放在实现部分中,您将如何创建其中一个并公开接口? Spring.Collections.TCollections.CreateList 如何访问 TObjectList?
      • @rudy 你有使用 Delphi 界面的经验吗?我的意思是现实世界的经验。
      • @rudy 在实现部分隐藏接口实现类并不能解决任何问题,只会带来麻烦(用于测试、继承等)。此外,它不会阻止程序员将接口引用转换为 TObject 和 f.i。释放该实例。所以一文不值
      • @RudyVelthuis 如果它不公开,你将如何继承它?您将如何准确测试该实现?如果隐藏,您必须依赖可能会返回该类进行测试的东西。我不喜欢那种隐藏,因为它带来的麻烦比它真正的帮助要多
      • 在这里与鲁迪争论没有什么意义。他显然不明白这些问题。
      【解决方案4】:

      Spring4D 的集合类被设计为与接口一起使用,TObjectList 实现了 IList,因此如果您使用接口引用它,它将按预期工作。

      procedure TForm1.FormCreate(Sender: TObject);
      var
        geometricFigures: IList<TGeometricFigure>;
        geometricFigure: TGeometricFigure;
      begin
        ReportMemoryLeaksOnShutdown := true;
      
        geometricFigures := TCollections.CreateObjectList<TGeometricFigure>(true);
        geometricFigures.Add(TCircle.Create(4,2));
        geometricFigures.Add(TCircle.Create(0,4));
        geometricFigures.Add(TRectangle.Create(3,10,4));
        geometricFigures.Add(TSquare.Create(1,5));
        geometricFigures.Add(TTriangle.Create(5,7,4));
        geometricFigures.Add(TTriangle.Create(2,6,3));
      
        for geometricFigure in geometricFigures do 
        begin
          geometricFigure.ToString();
        end;
      end;
      

      【讨论】:

        【解决方案5】:

        创建您自己的 TGemoetricFigures 列表来覆盖析构函数。然后你可以很快分辨出谁在调用析构函数。

        type
          TGeometricFigures = class(TObjectList<TGeometricFigure>)
          public
            destructor Destroy; override;
          end;
        
        implementation
        
        { TGeometricFigures }
        
        destructor TGeometricFigures.Destroy;
        begin
          ShowMessage('TGeometricFigures.Destroy was called');
          inherited;
        end;
        
        procedure FormCreate(Sender: TObject);
        var
          geometricFigures: TGeometricFigures;
          geometricFigure: TGeometricFigure;
        begin
          ReportMemoryLeaksOnShutdown := true;
        
          geometricFigures := TGeometricFigures.Create;
          try
            geometricFigures.Add(TCircle.Create(4,2));
            geometricFigures.Add(TCircle.Create(0,4));
            geometricFigures.Add(TRectangle.Create(3,10,4));
            geometricFigures.Add(TSquare.Create(1,5));
            geometricFigures.Add(TTriangle.Create(5,7,4));
            geometricFigures.Add(TTriangle.Create(2,6,3));
        
            for geometricFigure in geometricFigures do begin
              geometricFigure.ToString();
            end;
          finally
            //geometricFigures.Free(); -> this line is not required (?)
          end;
        end;
        

        我的猜测是,geometricFigure.ToString() 内部的某些东西做了一些不应该发生的事情,因为副作用会破坏几何图形。使用 FastMM4 FullDebugMode,您可能会获得更多信息。

        【讨论】:

        • ToString() 不是问题(如果是这样的话,我会感到惊讶)。问题是构造函数中的_AddRef 和枚举器的析构函数中的_Release。两者都是,AFAICT,不必要的,但 _Release 是什么使 refcount 为 0,然后释放整个类。
        猜你喜欢
        • 2017-01-11
        • 2011-09-24
        • 1970-01-01
        • 2020-06-26
        • 2012-12-30
        • 2012-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多