La documentación, al menos desde D6, indica que si crea un objeto de la clase TObjectList con la propiedad OwnsObjects en true (que dicho de paso es su valor por defecto) el objeto asume la responsabilidad de liberar los items cuando sea necesario. Para ser exactos la ayuda dice:
La parte que nos compete a analizar es el final: "Si la propiedad OwnsObjects es establecida a verdadero, TObjectList controla la memoria de dichos objetos liberando al objeto cuando su índice es reasignado, es removido de la lista con los métodos Delete, Remove o Clear; o cuando la instancia de TObjectList es destruída por si misma".Use TObjectList to store and maintain a list of objects. TObjectList provides properties and methods to add, delete, rearrange, locate, access, and sort objects. If the OwnsObjects property is set to True (the default), TObjectList controls the memory of its objects, freeing an object when its index is reassigned; when it is removed from the list with the Delete, Remove, or Clear method; or when the TObjectList instance is itself destroyed.
El texto en ese sentido es muy claro y debería ser suficiente. Pero... cuando examino el código es cuando mi duda cobra fuerza.
Comencé por intentar comprender el método Delete. Al menos en D6, el método Delete de TobjectList no existe... es en realidad el que hereda de TList:
procedure TList.Delete(Index: Integer); var Temp: Pointer; begin if (Index < 0) or (Index >= FCount) then Error(@SListIndexError, Index); Temp := Items[Index]; Dec(FCount); if Index < FCount then System.Move(FList^[Index + 1], FList^[Index], (FCount - Index) * SizeOf(Pointer)); if Temp <> nil then Notify(Temp, lnDeleted); end;
La ayuda sobre este método dice:
Removes the item at the position given by the Index parameter.
procedure Delete(Index: Integer);
Description
Call Delete to remove the item at a specific position from the list. The index is zero-based, so the first item has an Index value of 0, the second item has an Index value of 1, and so on. Calling Delete moves up all items in the Items array that follow the deleted item, and reduces the Count.
In TList, the Extract and Delete methods behave exactly the same way. Descendant classes (including TObjectList and TComponentList) distinguish the two methods.
To remove the reference to an item without deleting the entry from the Items array and changing the Count, set the Items property for Index to nil.
Note: Delete does not free any memory associated with the item. To free the memory that was used to store a deleted item, set the Capacity property.
Lo que está en negrita es lo que yo quiero remarcar. Lo primero dice que en TList tanto Extract como Delete hacen exactamente lo mismo. Y aclara que las clases descendientes, incluída TObjectList y TComponetList hacen distinción entre estos 2 métodos.
Bueno, aquí es donde yo al menos percibo cierta incongruencia. Como he dicho: TObjectList no posee método Delete así que no puede hacer distinción. Veamos entonces lo que hace Extract:
function TObjectList.Extract(Item: TObject): TObject; begin Result := TObject(inherited Extract(Item)); end;
Ajá... ¡Invoca al de padre! ¿Entonces de que distinción habla? ¿Que hace Extract de TList? Esto:
function TList.Extract(Item: Pointer): Pointer; var I: Integer; begin Result := nil; I := IndexOf(Item); if I >= 0 then begin Result := Item; FList^[I] := nil; Delete(I); Notify(Result, lnExtracted); end; end;
Se puede ver efectivamente que al final se resume a un Delete. Por tanto ya sea que se haga un Extact() como Delete() con TList o un TObjectList se hace lo mismo. Yo me digo entonces que no puede ser... algo está mal o yo estoy interpretando y comprendiendo mal.
El segundo texto en negrita es una nota por demás inquietante: Delete no libera la memoria asociada al item borrado. Para ello hay que establecer la propiedad Capacity.
¿Entonces que implica o que hace Delete?
Del código se puede desprender lo que hace es decrementar la cantidad, mover los bytes asociados al item de su posición al principio. De este modo cuantos más deletes se hagan estos items marcados como borrados se posicionen al comienzo y dejar al resto al final.
Esto ya me estaba oliendo sospechoso, y confuso. Hay un método a destacar... Notify. Cada vez que se agrega, borra o mueve un item se envía un Notify con la operación en cuestión. Este método es virtual, y en TList está vacio. Me dije entonces que efectivamente entonces será que TObjectList asume la responsabilidad. Así es:
procedure TObjectList.Notify(Ptr: Pointer; Action: TListNotification); begin if OwnsObjects then if Action = lnDeleted then TObject(Ptr).Free; inherited Notify(Ptr, Action); end;
¡Al fin veo que participe de algo la famosa propiedad OwnsObjects!. Se puede ver del código que la única comprobación sobre las operaciones corresponde a un Delete. Desde el método Delete se aprecia que efectivamente se manda un Notify con la acción lnDeleted y por tanto al menos puedo confirmar que para el caso de TObjectList se ocupa de hacer liberar el item cuando se invoque a Delete.
¿Pero que pasa con las otras opciones? La ayuda dice esto exactamente sobre la propiedad OwnsObjects:
Allows TObjectList to free objects when they are deleted from the list or the list is destroyed.
property OwnsObjects: Boolean;
Description
OwnsObjects allows TObjectList to control the memory of its objects. If OwnsObjects is True (the default),
calling Delete or Remove frees the deleted object in addition to removing it from the list.
calling Clear frees all the objects in the list in addition to emptying the list.
calling the destructor frees all the objects in the list in addition to destroying the TObjectList itself.
assigning a new value to an index in Items frees the object that previously occupied that position in the list.
Even if OwnsObjects is True, the Extract method can be used to remove objects from the list without freeing them.
Remove, invoca al de su padre. Cuya implementación es:
function TList.Remove(Item: Pointer): Integer; begin Result := IndexOf(Item); if Result >= 0 then Delete(Result); end;
Bien.... tiene lugar la liberación del item.
Clear es heredado de su padre:
procedure TList.Clear; begin SetCount(0); SetCapacity(0); end;
Como me encanta rastrillar... me meto a estos métodos:
procedure TList.SetCount(NewCount: Integer); var I: Integer; begin if (NewCount < 0) or (NewCount > MaxListSize) then Error(@SListCountError, NewCount); if NewCount > FCapacity then SetCapacity(NewCount); if NewCount > FCount then FillChar(FList^[FCount], (NewCount - FCount) * SizeOf(Pointer), 0) else for I := FCount - 1 downto NewCount do Delete(I); FCount := NewCount; end;
Bueno... obviando los controles, se ve que aplica el Delete para que luego el Notify de TObjectList haga lo suyo. Veamos lo que hace SetCapacity:
procedure TList.SetCapacity(NewCapacity: Integer); begin if (NewCapacity < FCount) or (NewCapacity > MaxListSize) then Error(@SListCapacityError, NewCapacity); if NewCapacity <> FCapacity then begin ReallocMem(FList, NewCapacity * SizeOf(Pointer)); FCapacity := NewCapacity; end; end;
Muy bien... relocaliza toda la lista según la capacidad establecida.
La documentación sugiere que se trabaje con Capacity en lo posible ya que ayuda a evitar estar haciendo más relocalizaciones de memoria innecesarias a establecer una capacidad determinada. Puede expandirse en cuanto sea necesario, y lo hace de en función de que tan grande sea la capacidad actual. Si es > 64, se expande un 25% mientras que si la capacidad es > 8 (y menor o igual a 64, por tanto) se expande de a 8, de lo contrario de 4 en 4.
Hasta allí voy comprendiendo.
Se dice que también TObjectList toma el mando cuando se libera éste. El punto es que no tiene destructor sobreescrito, tiene lugar el de TList:
destructor TList.Destroy; begin Clear; end;
Y cuando se reasigna o acomoda los índices del item. El Insert() que se ejecuta es del padre. Y allí no observo algún control por parte del TObjectList:
procedure TList.Insert(Index: Integer; Item: Pointer); begin if (Index < 0) or (Index > FCount) then Error(@SListIndexError, Index); if FCount = FCapacity then Grow; if Index < FCount then System.Move(FList^[Index], FList^[Index + 1], (FCount - Index) * SizeOf(Pointer)); FList^[Index] := Item; Inc(FCount); if Item <> nil then Notify(Item, lnAdded); end;
Más bien solo corre la lista hacia arriba para luego asociar a dicha posición el item a agregar. Si se manda a llamar a Move() heredado de TList lo que se hace es borrarlo de la lista y volver a insertar:
procedure TList.Move(CurIndex, NewIndex: Integer); var Item: Pointer; begin if CurIndex <> NewIndex then begin if (NewIndex < 0) or (NewIndex >= FCount) then Error(@SListIndexError, NewIndex); Item := Get(CurIndex); FList^[CurIndex] := nil; Delete(CurIndex); Insert(NewIndex, nil); FList^[NewIndex] := Item; end; end;
Entonces yo me quedo preocupado si efectivamente el TObjectList efectivamente asume todo el control como debiera... El sólo recordar que su Notify() sólo evalúa las eliminaciones y no necesariamente una inserción (no al menos con Add), y cuya implementación delegada hacia su padre hace que se resuma en ir poniendo los elementos "no borrados" hacia el final y los "eliminados" al principio me pone en estado de alerta.
Y la nota que deja el Delete() sobre que debe emplearse Capacity() para efectivamente borrar la memoria... ya me abruma:
Si en realidad gracias al Notify sobreescrito de TObjectList tiene lugar el correspondiente Free del item, ¿que es lo que hace el Capacity? ¿Al final debo utilizar Capacity() aún cuando se está empleando OwnsObjects en true? ¿O es que una cosa es la memoria que ocupa cada instancia de un ítem y otra es la propia memoria de la lista?
Tengo el cerebro frito desde el lunes. ¿Alguien podría evacuarme estas dudas y aclararme mejor el panorama?
Saludos,