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

时间:2016-07-02 17:00:26

标签: delphi spring4d

我对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循环则不会释放?

更新

我遵循了塞巴斯蒂安的建议并调试了析构函数。列表项被以下代码破坏:

{$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进行开发时没有足够的经验来评估所描述的行为是否真的是一个错误,因此撤销了我接受的答案,对不起。

5 个答案:

答案 0 :(得分:8)

要使用for ... do进行迭代,该类必须具有GetEnumerator方法。这显然会使自己(即TObjectList<>)返回IEnumerator<TGeometricFigure> 接口。迭代后,释放IEnumerator<>,其引用计数达到0,并释放对象列表。

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

在Delphi中,正如您所看到的,这是一个问题。我想解决方案是TObjectList<>有一个单独的(可能是嵌套的)类或记录进行枚举,而不是返回Self(作为IEnumerator<>)。但这取决于Spring4D的作者。你可以把这个问题引起Stefan Glienke的注意。

更新

您的附录显示,这并非如此。 TObjectList<>(或者更确切地说,它的祖先TList<>)返回一个单独的枚举器,但这样做(IMO完全没必要,即使列表从头开始用作接口){{1 } / _AddRef而后者是罪魁祸首。

注意

我看到多个声称在Spring4D中,该类不应该用作类。然后,这些类不应该在_Release部分中公开,而是在单元的interface部分中公开。如果暴露了这样的类,作者应该期望用户使用它们。如果它们可用作类,则implementation循环不应释放容器。其中一个是设计问题:暴露为类或自动释放。所以有一个错误,IMO。

答案 1 :(得分:4)

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

TObjectList<T>旨在用作接口并具有引用计数。只要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将在内部调用_AddRef上的olist并将引用计数增加到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,它将在内部调用_Release上的olist并将refcount减少为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,它将在内部调用_Release上的olist并将refcount减少为0暗示olist实例已被释放。

答案 2 :(得分:3)

您正在使用for in loop来迭代收集;这种循环在名为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调用返回一个接口,当你完成循环时,它将获得Freed。 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中,建议您不要将接口引用与对象引用混合

答案 3 :(得分:3)

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;

答案 4 :(得分:1)

创建自己的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,很可能会获得更多信息。