Почему список типов TObjectList освобождается автоматически после итерации?
У меня есть вопрос, касающийся поведения класса TObjectList платформы Spring4D. В своем коде я создаю список геометрических фигур, таких как 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 удален из кода?
Обновить
Я последовал совету Себастьяна и отладил деструктора. Элементы списка уничтожаются следующим кодом:
{$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;
Обновить
Я должен был пересмотреть мой принятый ответ и пришел к следующему выводу:
На мой взгляд, ответ Руди правильный, хотя описанное поведение не может быть ошибкой в рамках. Я думаю, что Руди дает хороший аргумент, указывая, что фреймворк должен работать так, как ожидалось. Когда я использую цикл for-in, я ожидаю, что это будет операция только для чтения. Очистка списка впоследствии - это не то, что я ожидал.
С другой стороны, Fritzw и David Heffernan указывают, что дизайн среды Spring4D основан на интерфейсе и поэтому должен использоваться таким образом. Пока это поведение задокументировано (возможно, Fritzw мог бы дать нам ссылку на документацию), я согласен с Дэвидом, что мое использование фреймворка неверно, хотя я все еще думаю, что поведение фреймворка ошибочно.
Я не достаточно опытен в разработке с Delphi, чтобы оценить, является ли описанное поведение действительно ошибкой или нет, поэтому отменил мой принятый ответ, извините за это.
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
Перечислитель увеличит счет olist
до 1
begin
o.ToString();
end;
Перечислитель выходит из области видимости и вызывается деструктор перечислителя, который уменьшает счет 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
внутренне позвонит _AddRef
на olist
и увеличить счет до 1.
try
olist.Add( TFoo.Create() );
olist.Add( TFoo.Create() );
for o in olist do
Перечислитель увеличит счет olist
до 2
begin
o.ToString();
end;
Перечислитель выходит из области видимости и вызывается деструктор перечислителя, который уменьшает счет olist
до 1.
finally
//olist.Free(); -> this line is not required (?)
end;
end;
В конце процедуры переменная интерфейса olisti
будет установлен в nil
, который будет внутренне вызывать _Release
на olist
и уменьшить счет до 0, и это означает, что olist
экземпляр освобожден.
То же самое происходит, когда мы назначаем ссылку непосредственно из конструктора в переменную интерфейса:
procedure foo;
var
olist: IList<TFoo>;
o: TFoo;
begin
olist := TObjectList<TFoo>.Create();
Присвоение ссылки на интерфейсную переменную olist
внутренне позвонит _AddRef
и увеличить счет до 1.
olist.Add( TFoo.Create() );
olist.Add( TFoo.Create() );
for o in olist do
Перечислитель увеличит счет olist
до 2
begin
o.ToString();
end;
Перечислитель выходит из области видимости и вызывается деструктор перечислителя, который уменьшает счет olist
до 1.
end;
В конце процедуры переменная интерфейса olist
будет установлен в nil
, который будет внутренне вызывать _Release
на olist
и уменьшить счет до 0, и это означает, что olist
экземпляр освобожден.
Сделать итерацию с for ... do
класс должен иметь GetEnumerator
метод. Это, очевидно, возвращает себя (т.е. TObjectList<>
) как IEnumerator<TGeometricFigure>
интерфейс После итерации IEnumerator<>
освобождается, его счетчик ссылок достигает 0, а список объектов освобождается.
Это шаблон, который вы часто видите, скажем, в C#, но там он не имеет такого эффекта, потому что на экземпляр класса все еще ссылаются, и сборщик мусора не будет подключаться.
Однако, как видите, в Delphi это проблема. Я думаю, что решение будет для TObjectList<>
иметь отдельный (возможно, вложенный) класс или запись, которая выполняет перечисление, и не возвращать Self
(как IEnumerator<>
). Но это зависит от автора Spring4D. Вы могли бы довести эту проблему до сведения Стефана Глиенке.
Обновить
Ваше приложение показывает, что это не совсем то, что происходит. TObjectList<>
(или, если быть более точным, его предок TList<>
) возвращает отдельный перечислитель, но это делает (IMO совершенно не нужен, даже если список используется как интерфейс с самого начала) _AddRef
/_Release
и последний является виновником.
Заметка
Я вижу множество заявлений о том, что в Spring4D класс не должен использоваться как класс. Тогда такие классы не должны выставляться в interface
раздел, но в implementation
раздел блока вместо. Если такие классы выставлены, автор должен ожидать, что пользователь будет использовать их. И если они могут быть использованы как класс, то for-in
петля не должна освобождать контейнер. Одной из них является проблема дизайна: либо экспозиция как класс, либо автоматическое освобождение. Так что есть ошибка, ИМО.
Вы используете for in loop
выполнить итерацию по коллекции; такого рода цикл ищет метод в классе под названием GetEnumerator
, В Spring4D, для TObjectList<T>
в итоге вы называете унаследованным 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
call возвращает интерфейс, когда вы закончите цикл, он получит Freed. Destructor
реализован так:
destructor TList<T>.TEnumerator.Destroy;
begin
fList._Release;
inherited Destroy;
end;
Обратите внимание, что это вызывает _Release
в списке. Если вы перейдете к использованию отладчика, вы заметите, что он уменьшает RefCount
списка до 0, а затем он вызывает _Release
так вот почему ваш список освобождается
Если вы удалите цикл for в исходном коде, у вас будет утечка памяти:
Неожиданная утечка памяти
Произошла непредвиденная утечка памяти. Неожиданные небольшие утечки блоков:
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
Редактировать: только что увидел ответ Руди Велтуиса. Это не ошибка Spring4D. Вы не должны использовать фреймворки на основе классов коллекций. Вы должны использовать коллекции на основе интерфейса. Кроме того, не относится к Spring4D, но в Delphi рекомендуется не смешивать ссылки на интерфейсы с ссылками на объекты
Классы коллекций 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;
Создайте свой собственный список 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;
Я предполагаю, что что-то внутри geometryFigure.ToString() делает что-то, что не должно происходить, что в качестве побочного эффекта разрушает геометрические значения. Используйте FastMM4 FullDebugMode, и, скорее всего, вы получите больше информации.