8

我对 Spring4D 框架的 TObjectList 类的行为有疑问。在我的代码中,我创建了一个几何图形列表,例如square, circle, triange,每个都定义为一个单独的类。为了在列表被破坏时自动释放几何图形,我定义了一个 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 开发方面没有足够的经验来评估所描述的行为是否实际上是一个错误,或者没有因此撤销我接受的答案,对此感到抱歉。

4

5 回答 5

9

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

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

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

更新

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

笔记

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

于 2016-07-02T18:27:48.960 回答
5

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

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 

枚举器将 refcountolist增加到 1

    begin
      o.ToString();
    end;

枚举器超出范围并调用枚举器的析构函数,这会将 refcount 减少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内部调用并将引用计数增加到 1。_AddRefolist

  try
    olist.Add( TFoo.Create() );
    olist.Add( TFoo.Create() );

    for o in olist do 

枚举器将 refcountolist增加到 2

    begin
      o.ToString();
    end;

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

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

在过程结束时,接口变量olisti将设置为,这将在nil内部调用并将 refcount 减少到 0,这意味着该实例已被释放。_Releaseolistolist

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

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

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

  olist.Add( TFoo.Create() );
  olist.Add( TFoo.Create() );

  for o in olist do 

枚举器将 refcountolist增加到 2

  begin
    o.ToString();
  end;

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

end;

在过程结束时,接口变量olist将设置为,这将在nil内部调用并将 refcount 减少到 0,这意味着该实例已被释放。_Releaseolistolist

于 2016-07-04T09:20:49.417 回答
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;
于 2016-07-03T22:30:36.753 回答
3

您正在使用 afor in loop来遍历集合;这种循环在名为GetEnumerator. 在 Spring4D 中,对于 a TObjectList<T>,您最终调用了inherit ,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调用返回一个接口,当您完成循环时,它将被释放。是这样实现的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 中,建议您不要将接口引用与对象引用混合

于 2016-07-02T18:46:34.003 回答
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() 内部的某些东西做了一些不应该发生的事情,因为副作用会破坏几何图形。使用 FastMM4 FullDebugMode,您可能会获得更多信息。

于 2016-07-02T18:18:43.577 回答