Why is the TObjectList type list automatically freed after iteration?

I have a question about the behavior of the TObjectList class of Spring4D. In my code, I create a list of geometric shapes, such as square , circle , triange , each of which is defined as a separate class. To automatically remove geometric shapes when the list is destroyed, I defined a list of TObjectList types as follows:

 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 geometricFigures list will be automatically freed from memory, even if I do not call the Free method on the list (note the line in the finally block). I expected a different behavior, I thought that the list needed an explicit call to Free (), because the local variable geometricFigures does not use the interface type.

I also noticed that if the list items are not repeated in the inner loop (I temporarily removed it from the code), the list is not freed automatically, and I get a memory leak.

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

Update

I followed Sebastian’s advice and debugged the destructor. List items are destroyed with 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 revise my accepted answer and came to the following conclusion:

In my opinion, Rudy's answer is correct, although the described behavior may not be an error within the framework. I think Rudy makes a good argument, indicating that the structure should work as expected. When I use the for-in loop, I expect this to be a read-only operation. Putting away the list afterwards is not what I expected.

On the other hand, Fritzw and David Heffernan indicate that the design of the Spring4D framework is based on the interface and therefore should be used in this way. While this behavior is documented (perhaps Fritzw can give us a link to the documentation) I agree with David that my use of the framework is incorrect, although I still believe that the framework’s behavior is skipped.

I am not experienced enough in development with Delphi to assess whether the described behavior is actually a mistake or not, so I withdrew my accepted answer, sorry for that.

+7
delphi spring4d
source share
5 answers

To understand why the list is being released, we need to understand what happens behind the scenes.

TObjectList<T> intended to be used as an interface and has reference counting. Whenever refcount reaches 0, the instance will be freed.

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

The return for olist now 0

  try olist.Add( TFoo.Create() ); olist.Add( TFoo.Create() ); for o in olist do 

Enumerator will increase olist recount to 1

  begin o.ToString(); end; 

The enumerator goes out of scope and the enumerator destructor is called, which will reduce the olist from olist to 0, which means that the olist instance olist 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 is 0

  olisti := olist; 

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

  try olist.Add( TFoo.Create() ); olist.Add( TFoo.Create() ); for o in olist do 

Enumerator will increase olist recount to 2

  begin o.ToString(); end; 

The enumerator goes out of scope and the enumerator destructor is called, which reduces the conversion of olist to 1.

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

At the end of the procedure, the olisti interface variable will be set to nil , which internally calls _Release on olist and reduces the refcount to 0, which means that the olist instance olist freed.

The same thing happens when we assign a direct from constructor link to an interface variable:

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

Assigning a reference to an olist interface olist will internally call _AddRef and increase refcount to 1.

  olist.Add( TFoo.Create() ); olist.Add( TFoo.Create() ); for o in olist do 

Enumerator will increase olist recount to 2

  begin o.ToString(); end; 

The enumerator goes out of scope and the enumerator destructor is called, which reduces the conversion of olist to 1.

 end; 

At the end of the procedure, the interface variable olist will be set to nil , which internally calls _Release on olist and reduces the value of refcount to 0, which means that the olist instance olist freed.

+4
source share

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

This is a pattern that you often see, say, in C #, but there it does not have this effect, because the class instance is still referenced and the garbage collector will not be included.

In Delphi, however, this is a problem, as you can see. I think the solution would be for TObjectList<> have a separate (possibly nested) class or record that does the enumeration, rather than returning Self (like IEnumerator<> ). But it depends on the author of Spring4D. You can pay attention to this problem on Stefan Glinka.

Update

Your addition shows that this is not quite what is happening. TObjectList<> (or rather, its ancestor TList<> ) returns a separate enumerator, but it does (IMO is completely unnecessary, even if this list is used as an interface from the very beginning) _AddRef / _Release and the latter is the culprit.

Note

I see several claims that in Spring4D a class should not be used as a class. Then such classes should not be displayed in the interface section, but in the device implementation section. If such classes are open, the author should expect the user to use them. And if they are suitable for use as a class, then the for-in loop should not free the container. One of them is the design problem: either exposure as a class, or automatic release. So there is a mistake, IMO.

+8
source share

You use for in loop to iterate through the collection; this type of loop looks for a method in a class called GetEnumerator . In Spring4D, for TObjectList TObjectList<T> you call the inherited TList<T>.GetEnumerator , which is implemented as:

 function TList<T>.GetEnumerator: IEnumerator<T>; begin Result := TEnumerator.Create(Self); end; 

And the constructor for TEnumerator is implemented as:

 constructor TList<T>.TEnumerator.Create(const list: TList<T>); begin inherited Create; fList := list; fList._AddRef; fVersion := fList.fVersion; end; 

Note that it will call _AddRef in the list. At this point, your TObjetList RefCount goes to 1

Since calling GetEnumerator returns an interface when you finish the loop, it will get Freed. Destructor is executed as follows:

 destructor TList<T>.TEnumerator.Destroy; begin fList._Release; inherited Destroy; end; 

Note that it calls _Release on the list. If you start using the debugger, you will notice that it reduces the RefCount list to 0, and then calls _Release , so therefore your list is freed

If you remove the for in loop in the source code, you will get a memory leak:


Unexpected memory leak

An unexpected memory leak has occurred. Unexpected small block leaks are:

1 - 12 bytes: TGeometricFigure x 6, TMoveArrayManager x 1, Unknown x 1

21 - 28 bytes: TList x 1

29 - 36 bytes: TCriticalSection x 1

53 - 60 bytes: TCollectionChangedEventImpl x 1, Unknown x 1

77 - 84 bytes: TObjectList x 1

Edit: Just saw Rudy Veltius. This is not a Spring4D bug. You should not use a collection based on frameworks. You must use a collection based on the interface. Also, not related to Spring4D, but in Delphi, you are advised not to mix interface links with object links

+3
source share

Spring4D collection styles are designed for use with interfaces, TObjectList implements IList, so if you reference it using an interface, it will work as expected.

 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; 
+3
source share

Create your own list of TGemoetricFigures, which overrides the destructor. Then you can tell pretty quickly who is calling the destructor.

 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; 

My guess is that something inside geometricFigure.ToString () is doing something that shouldn't happen, as a side effect destroys geometric shapes. Use FastMM4 FullDebugMode, and most likely you will get more information.

+1
source share

All Articles