Почему список типов 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 в списке. В этот момент ваш TObjetListRefCount идет к 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, и, скорее всего, вы получите больше информации.

Другие вопросы по тегам