Search code examples
delphispring4d

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.


Solution

  • 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.