Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is list of type TObjectList freed automatically after iteration?

I have a question regarding the behaviour of the TObjectList class of the Spring4D framework. In my code I create a list of geometric figures such as square, circle, triange, each defined as an individual class. To free the geometric figures automatically when the list gets destroyed I defined a list of type TObjectList like this:

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;

If I run this code the list geometricFigures is freed automatically from memory even if I am not calling the method Free on the list (notice commented out line in the finally block). I expected a different behaviour, I thought the list needs an explicit call to Free() because the local variable geometricFigures is not using an interface type.

I further noticed that if the items of the list are not iterated in the for-in loop (I removed it from the code temporarily), the list is not freed automatically and i get a memory leak.

This leads me to the following question: Why is the list of type TObjectList (geometricFigures) freed automatically when its items are iterated but not if the for-in loop is removed from the code?

Update

I followed the advice of Sebastian and debugged the destructor. The list items get destroyed by the following code:

{$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;

Update

I had to reconsider my accepted answer and came to the following conclusion:

In my opinion Rudy's answer is correct even though the described behaviour might not be a bug in the framework. I think Rudy makes a good argument by pointing out that a framework should work as expected. When I am using a for-in loop I expect it to be a read-only operation. Clearing the list afterwards is not what I expected to happen.

On the other hand Fritzw and David Heffernan are pointing out that the design of the Spring4D framework is interface based and therefore should be used in that way. As long as this behaviour is documented (maybe Fritzw could give us a reference to the documentation) I agree with David that my usage of the framework is not correct even though I still think that the behaviour of the framework is missleading.

I am not experienced enough in developing with Delphi to evaluate if the described behaviour is actually a bug or not therefore revoked my accepted answer, sorry about that.

like image 384
MUG4N Avatar asked Jul 02 '16 17:07

MUG4N


2 Answers

To do an iteration with for ... do, the class must have a GetEnumerator method. This obviously returns itself (i.e. the TObjectList<>) as an IEnumerator<TGeometricFigure> interface. After the iteration, the IEnumerator<> is released, its reference count reaches 0, and the objectlist is freed.

This is a pattern you often see in, say, C#, but there, it doesn't have this effect, because the class instance is still referenced and the garbage collector will not jump in.

In Delphi however, this is a problem, as you can see. I guess the solution would be for the TObjectList<> to have a separate (possibly nested) class or record that does the enumeration, and not to return Self (as IEnumerator<>). But this is up to the author of Spring4D. You could bring this problem to the attention of Stefan Glienke.

Update

Your addendum shows that this is not exactly what happens. The TObjectList<> (or to be more precise, its ancestor TList<>) returns a separate enumerator, but that does an (IMO totally unnecessary, even if the list is used as interface from the start) _AddRef/_Release and the latter is the culprit.

Note

I see multiple claims that in Spring4D, the class should not be used as class. Then such classes should not be exposed in the interface section, but in the implementation section of the unit instead. If such classes are exposed, the author should expect the user to use them. And if they are usable as class, then a for-in loop should not free the container. One of these is a design problem: either the exposure as class, or the auto-freeing. So there is a bug, IMO.

like image 191
Rudy Velthuis Avatar answered Sep 23 '22 20:09

Rudy Velthuis


To understand why the list is freed we need to understand what is going on behind the scenes.

TObjectList<T> is designed to use as an interface and has reference counting. Whenever the refcount reaches 0 the instance will be freed.

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

The refcount for olist is now at 0

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

    for o in olist do 

The enumerator increase the refcount of olist to 1

    begin
      o.ToString();
    end;

The enumerator get out of scope and the destructor of the enumerator is called, which will decrease the refcount of olist to 0, and that implies that the olist instance is freed.

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

What is the difference when using an interface variable?

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

olist refcount is 0

  olisti := olist;

Assigning the olist reference to the interface variable olisti will internally call _AddRef on olist and increase the refcount to 1.

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

    for o in olist do 

The enumerator increase the refcount of olist to 2

    begin
      o.ToString();
    end;

The enumerator get out of scope and the destructor of the enumerator is called, which will decrease the refcount of olist to 1.

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

At the end of the procedure the interface variable olisti will be set to nil, which will internally call _Release on olist and decrease the refcount to 0 and that implies that the olist instance is freed.

The same happens when we assign the reference direct from constructor to the interface variable:

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

Assigning the reference to the interface variable olist will internally call _AddRef and increase the refcount to 1.

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

  for o in olist do 

The enumerator increase the refcount of olist to 2

  begin
    o.ToString();
  end;

The enumerator get out of scope and the destructor of the enumerator is called, which will decrease the refcount of olist to 1.

end;

At the end of the procedure the interface variable olist will be set to nil, which will internally call _Release on olist and decrease the refcount to 0 and that implies that the olist instance is freed.

like image 38
Sir Rufo Avatar answered Sep 26 '22 20:09

Sir Rufo