为什么 TObjectList 类型的列表在迭代后自动释放?
Why is list of type TObjectList freed automatically after iteration?
我对 Spring4D 框架的 TObjectList class 的行为有疑问。在我的代码中,我创建了一个几何图形列表,例如 square
、circle
、triange
,每个定义为一个个体 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
实例已被释放。
我对 Spring4D 框架的 TObjectList class 的行为有疑问。在我的代码中,我创建了一个几何图形列表,例如 square
、circle
、triange
,每个定义为一个个体 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
实例已被释放。