Почему список типов 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 on список (Примечание закомментированная строка в блоке finally). Я ожидал другого поведения, я думал, что список нуждается в явном вызове Free (), потому что локальная переменная geometricFigures не использует тип интерфейса.

Я также заметил, что если элементы списка не повторяются в цикле for-in (я временно удалил его из кода), список не освобождается автоматически, и я получаю утечку памяти.

это приводит меня к следующему вопросу: Почему список TObjectList тип (geometricFigures) освобождается автоматически, когда его элементы повторяются, но не если цикл for-in удаляется из кода?

обновление

{$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 и Дэвид Хеффернан указывают, что дизайн spring4d framework основан на интерфейсе и поэтому должен использоваться таким образом. Пока такое поведение документированный (возможно, Fritzw мог бы дать нам ссылку на документацию) я согласен с Дэвидом, что мое использование фреймворка неверно, хотя я все еще думаю, что поведение фреймворка является пропуском.

Я недостаточно опытен в разработке с Delphi, чтобы оценить, действительно ли описанное поведение является ошибкой или Поэтому не отозвало мой принятый ответ, извините за это.

5 ответов


понять почему список освободили нам нужно понять, что происходит за кулисами.

TObjectList<T> предназначено для использования в качестве интерфейса и подсчет ссылок. Всякий раз, когда счетчик достигает 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 

перечислитель увеличивает refcount olist к 1

    begin
      o.ToString();
    end;

перечислитель выходит за рамки и вызывается деструктор перечислителя, который будет уменьшать refcount 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 refcount равно 0

  olisti := olist;

назначение olist ссылка на переменную интерфейс olisti внутренне называю _AddRef on olist и увеличьте refcount до 1.

  try
    olist.Add( TFoo.Create() );
    olist.Add( TFoo.Create() );

    for o in olist do 

в перечислитель увеличить refcount olist в 2

    begin
      o.ToString();
    end;

перечислитель выходит из области видимости, и вызывается деструктор перечислителя, который уменьшит refcount olist для 1.

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

в конце процедуры переменная интерфейса olisti будет установлен в nil, который будет внутренне называть _Release on olist и уменьшите refcount до 0, и это означает, что olist экземпляр освобождается.

то же происходит, когда мы назначаем ссылку непосредственно из конструктора переменной интерфейса:

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

назначение ссылки на переменную интерфейса olist внутренне называю _AddRef и увеличьте refcount до 1.

  olist.Add( TFoo.Create() );
  olist.Add( TFoo.Create() );

  for o in olist do 

перечислитель увеличивает refcount olist в 2

  begin
    o.ToString();
  end;

перечислитель выходит из области видимости, и вызывается деструктор перечислителя, который уменьшит refcount olist для 1.

end;

в конце процедуры переменная интерфейса olist будет установлен в nil, который будет внутренне называть _Release on olist и уменьшите refcount до 0, и это означает, что olist экземпляр освобождается.


для выполнения итерации с for ... do, класс должен иметь GetEnumerator метод. Это, очевидно, возвращает себя (т. е. TObjectList<>) в качестве IEnumerator<TGeometricFigure> интерфейс. После итерации IEnumerator<> освобождается, его счетчик ссылок достигает 0, и objectlist освобождается.

это шаблон, который вы часто видите, скажем, в C#, но там он не имеет этого эффекта, потому что экземпляр класса все еще ссылается, и сборщик мусора не будет прыгать.

в Delphi, однако, это проблема, как вы можете видеть. Я думаю, решение было бы для TObjectList<> иметь отдельный (возможно, вложенный) класс или запись, которая выполняет перечисление, и не возвращать Self (as IEnumerator<>). Но это зависит от автора Spring4D. Вы могли бы довести эту проблему до сведения Стефана Гленке.

обновление

ваши дополнения показывает, что это не совсем то, что происходит. The TObjectList<> (точнее, его предок!--9-->) возвращает отдельный перечислитель, но это делает (IMO совершенно ненужным, даже если список используется в качестве интерфейса с самого начала)_AddRef/_Release и последний является виновником.

Примечание

я вижу несколько утверждений, что в Spring4D класс не должен использоваться как класс. Тогда такие классы не должны выставляться в , но в implementation секция блока вместо этого. Если такие классы открыты, автор должен ожидать пользователя использовать их. И если они могут использоваться как класс, то a for-in loop не должен освобождать контейнер. Одна из них-проблема дизайна: либо экспозиция как класс, либо автоматическое освобождение. Итак, есть ошибка, ИМО.


вы используете 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 вызов возвращает интерфейс, когда вы закончите цикл, он освободится. The Destructor реализован следующим образом:

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

обратите внимание, что он называет _Release в списке. Если вы используете отладчик, вы заметите, что он уменьшает RefCount из списка до 0, а затем он вызывает _Release, так вот почему ваш список освободившись

если вы удалите цикл for in в исходном коде, вы получите утечку памяти:


Неожиданный Памяти Утечка

произошла неожиданная утечка памяти. Неожиданные небольшие утечки блока:

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

редактировать: только что видел Руди Velthuis ответа. Это не Spring4D жук. Вы должны не используйте коллекции на основе классов Framework. Вы должны использовать интерфейс коллекций. Кроме того, не связано с 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;

Я предполагаю, что что-то внутри geometricFigure.ToString () делает что-то, что не должно происходить, что в качестве побочного эффекта разрушает geometricFigues. Используйте Fastmm4 FullDebugMode, и, вероятно, вы получите больше информации.