среда, 22 мая 2019 г.

Как замена Free на FreeAndNil может поломать логику

Этот пост посвящается всем любителям споров FreeAndNil vs Free. Покажу пример, когда бездумную замену второго на первое делать нельзя.
Для понимания проблемы, покажу историю развития вопроса. Понятно, что если задачу решать полностью с нуля, то её можно решить разными способами, и можно выбрать решение, когда FreeAndNil будет лучшим и правильным вариантом. Однако в исторической перспективе получилось так, как получилось, я увидел что FreeAndNil всё поломает и решил об этом написать заметку.

Есть у меня один фрейм (TFrame), на который динамически в Run-Time создаётся компонет - превью отчёта. Т.к. превью - это наследник от TComponent, то я использую свойство Owner при создании и не забочусь об явном уничтожении превью. Создание дёргается внутри некоторого метода, который может быть вызван из вне несколько раз, выглядит это примерно так:
procedure TMyPreviewFrame.SomeMethod(SomeParams);
begin
  if FfrxPreviewForm = nil then
  begin
    FfrxPreviewForm := TfrxPreviewForm.Create(Self);
    // далее настройка превью при превом создании
  end;
  // далее работа с превью по переданным SomeParams сверху
end;
Соответственно уничтожение FfrxPreviewForm происходит автоматически при уничтожении фрейма; к экземпляру FfrxPreviewForm за пределами или в деструкторе фрейма я не обращаюсь, поэтому явно обнулять переменную нет необходимости.

В какой-то момент времени, мне понадобилось сохранять текущее состояние превью при выходе из приложения. Сделал я это просто:
type
  TMyPreviewFrame = class(TFrame)
  private
    FfrxPreviewForm: TfrxPreviewForm;
    procedure SavePreviewOptions;
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  ..

procedure TMyPreviewFrame.SavePreviewOptions;
begin
  if FfrxPreviewForm = nil then
    Exit;
  ..
  // сохраняем свойства из FfrxPreviewForm
end;

procedure TMyPreviewFrame.Notification(AComponent: TComponent; Operation: TOperation);
begin
  if (Operation = opRemove) and (AComponent = FfrxPreviewForm) then
  begin
    SavePreviewOptions;
    FfrxPreviewForm := nil;
  end;
end;

В VCL это работает так: при уничтожении экземпляра фрейма, фрейм (как наследник от TComponent) перебирает все компоненты, которыми владеет, и уничтожает их. При этом компонент оповещает владельца о том, что сейчас будет уничтожен.
Чисто теоретически, SavePreviewOptions может стать публичным методом, поэтому в Notification я добавил очистку ссылки (хотя на текущий момент это не обязательно).

И вот настал момент, когда в методе SomeMethod понадобилась возможность уничтожить FfrxPreviewForm (с сохранением его состояния). Делаю я это примерно так:
procedure TMyPreviewForm.SomeMethod(SomeParams);
begin
  if SomeCondition1(SomeParams) then
  begin
    FfrxPreviewForm.Free;
    Exit;
  end; 
  
  if FfrxPreviewForm = nil then
  begin
    FfrxPreviewForm := TfrxPreviewForm.Create(Self);
    // далее настройка превью при превом создании
  end;
  // далее работа с превью по переданным SomeParams сверху
end;

Надеюсь Вы понимаете, что тут происходит.
А теперь пища для ума: что будет, если FfrxPreviewForm.Free заменить на FreeAndNil(FfrxPreviewForm)?

4 коммент.:

Va-Bank комментирует...

И что будет? Не вызовется деструктор и тем самым не уничтожатся компоненты во фрейме

GunSmoker комментирует...

Ну то есть FreeAndNil помог найти логическую проблему: обращение к компоненту в момент его удаления (читай: в переходном состоянии) ;)

Николай Зверев комментирует...

Вы оба не правы. Вспомните о том, что FreeAndNil - он на самом деле работает как NilThenFree.
Т.е. при FreeAndNil(FfrxPreviewForm) сначала произойдёт установка переменной FfrxPreviewForm в nil, затем дёрнется деструктор превью, который дёрнет Notification. И вот в нём условие (AComponent = FfrxPreviewForm) не отработает, а значит и не вызовется SavePreviewOptions.

Т.е. утечек памяти не будет, но логика (когда при уничтожении превью надо сохранять его состояние) поломается. И такое не поддаётся диагностике.

Анонимный комментирует...

Согласен с GunSmoker, надо изменять логику сохранения

Отправить комментарий

.

.