为什么 TObjectList 类型的列表在迭代后自动释放?

Why is list of type TObjectList freed automatically after iteration?

我对 Spring4D 框架的 TObjectList class 的行为有疑问。在我的代码中,我创建了一个几何图形列表,例如 squarecircletriange,每个定义为一个个体 class。为了在列表被销毁时自动释放几何图形,我定义了一个 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 (注意最后一行注释掉了)堵塞)。我预计会有不同的行为,我认为列表需要显式调用 Free() 因为局部变量 geometricFigures 没有使用接口类型。

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

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

更新

我听从了塞巴斯蒂安的建议,调试了析构函数。列表项被以下代码销毁:

{$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 进行开发方面经验不足,无法评估所描述的行为是否真的是错误,因此撤销我接受的答案,对此深表歉意。

创建您自己的覆盖析构函数的 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() 内部的某些东西做了一些不应该发生的事情,因为副作用会破坏 geometricFigues。使用 FastMM4 FullDebugMode,您可能会获得更多信息。

要使用 for ... do 进行迭代,class 必须具有 GetEnumerator 方法。这显然是 return 本身(即 TObjectList<>)作为一个 IEnumerator<TGeometricFigure> 接口 。迭代后,IEnumerator<>被释放,其引用计数为0,objectlist被释放。

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

然而,在 Delphi 中,这是一个问题,如您所见。我想解决方案是 TObjectList<> 有一个单独的(可能嵌套的)class 或记录来进行枚举,而不是 return Self(因为 IEnumerator<>).不过这要看Spring4D的作者了。您可以将此问题报告给 Stefan Glienke。

更新

您的附录表明实际情况并非如此。 TObjectList<>(或者更准确地说,它的祖先 TList<>)return 是一个单独的枚举器,但这确实是一个(IMO 完全没有必要,即使列表从一开始就用作接口) _AddRef/_Release 而后者是罪魁祸首。

备注

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

您正在使用 for in loop 遍历集合;这种循环在 class 中寻找一个名为 GetEnumerator 的方法。在Spring4D中,对于一个TObjectList<T>,你最终调用了继承的TList<T>.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 调用了 returns 一个接口,当你完成循环时它将被释放。 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 错误。您应该 使用基于框架class 的集合。您必须 使用基于接口的集合。另外,与Spring4D无关,但在Delphi中,建议您不要将接口引用与对象引用混用

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;

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

TObjectList<T> 旨在用作接口并具有引用计数。每当引用计数达到 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 实例已被释放。