Лучшая практика для выполнения вложенного оператора TRY / FINALLY

Привет. Каков наилучший способ делать вложенные операторы try & finally в delphi?

var cds1  : TClientDataSet;
    cds2  : TClientDataSet;
    cds3  : TClientDataSet;
    cds4  : TClientDataSet;
begin
  cds1      := TClientDataSet.Create(application );
  try
    cds2      := TClientDataSet.Create(application );
    try
      cds3      := TClientDataSet.Create(application );
      try
        cds4      := TClientDataSet.Create(application );
        try
        ///////////////////////////////////////////////////////////////////////
        ///      DO WHAT NEEDS TO BE DONE
        ///////////////////////////////////////////////////////////////////////
        finally
          cds4.free;
        end;

      finally
        cds3.free;
      end;
    finally
      cds2.free;
    end;
  finally
    cds1.free;
  end;
end;

Можете ли вы предложить лучший способ сделать это?

6 ответов

Решение

Как насчет следующего:

var cds1  : TClientDataSet;
    cds2  : TClientDataSet;
    cds3  : TClientDataSet;
    cds4  : TClientDataSet;
begin
  cds1      := Nil;
  cds2      := Nil;
  cds3      := Nil;
  cds4      := Nil;
  try
    cds1      := TClientDataSet.Create(nil);
    cds2      := TClientDataSet.Create(nil);
    cds3      := TClientDataSet.Create(nil);
    cds4      := TClientDataSet.Create(nil);
    ///////////////////////////////////////////////////////////////////////
    ///      DO WHAT NEEDS TO BE DONE
    ///////////////////////////////////////////////////////////////////////
  finally
    freeandnil(cds4);
    freeandnil(cds3);
    freeandnil(cds2);
    freeandnil(Cds1);
  end;
end;

Это делает его компактным, и только пытается освободить экземпляры, которые были созданы. В действительности нет необходимости выполнять вложение, так как ЛЮБОЙ сбой приведет к переходу в окончание и выполнению всей очистки в приведенном вами примере.

Лично я стараюсь не вкладывать в один и тот же метод... за исключением того, что это сценарий "попытка / попытка / исключение / окончание". Если я чувствую, что мне нужно гнездиться, то для меня это прекрасное время, чтобы подумать о рефакторинге в вызов другого метода.

РЕДАКТИРОВАТЬ Немного mghie благодаря комментариям mghie и utku_karatas.

EDIT изменил создание объекта, чтобы не ссылаться на приложение, так как в этом примере это не обязательно.

Я бы использовал что-то вроде этого:

var
  Safe: IObjectSafe;
  cds1 : TClientDataSet;
  cds2 : TClientDataSet;
  cds3 : TClientDataSet;
  cds4 : TClientDataSet;
begin
  Safe := ObjectSafe;
  cds1 := Safe.Guard(TClientDataSet.Create(nil)) as TClientDataSet;
  cds2 := Safe.Guard(TClientDataSet.Create(nil)) as TClientDataSet;
  cds3 := Safe.Guard(TClientDataSet.Create(nil)) as TClientDataSet;
  cds4 := Safe.Guard(TClientDataSet.Create(nil)) as TClientDataSet;
  ///////////////////////////////////////////////////////////////////////
  ///      DO WHAT NEEDS TO BE DONE
  ///////////////////////////////////////////////////////////////////////

  // if Safe goes out of scope it will be freed and in turn free all guarded objects
end;

Для реализации интерфейса см. Эту статью, но вы можете легко создать что-то подобное самостоятельно.

РЕДАКТИРОВАТЬ:

Я только что заметил, что в связанной статье Guard() это процедура. В моем собственном коде я перегружен функциями Guard(), которые возвращают TObject, приведенный выше пример кода предполагает нечто подобное. Конечно, с дженериками гораздо лучший код теперь возможен...

РЕДАКТИРОВАТЬ 2:

Если вы удивляетесь, зачем пытаться... наконец-то полностью удалено из моего кода: невозможно удалить вложенные блоки без возможности утечки памяти (когда деструкторы вызывают исключения) или нарушения доступа. Поэтому лучше использовать вспомогательный класс, и пусть подсчет ссылок интерфейсов возьмет на себя полностью. Вспомогательный класс может освободить все объекты, которые он защищает, даже если некоторые из деструкторов вызывают исключения.

Есть еще один вариант кода без вложенной попытки... наконец, это только что пришло мне в голову. Если вы не создаете компоненты с параметром AOwner конструктора, установленным в nil, то вы можете просто использовать управление временем жизни, которое VCL предоставляет вам бесплатно:

var
  cds1: TClientDataSet;
  cds2: TClientDataSet;
  cds3: TClientDataSet;
  cds4: TClientDataSet;
begin
  cds1 := TClientDataSet.Create(nil);
  try
    // let cds1 own the other components so they need not be freed manually
    cds2 := TClientDataSet.Create(cds1);
    cds3 := TClientDataSet.Create(cds1);
    cds4 := TClientDataSet.Create(cds1);

    ///////////////////////////////////////////////////////////////////////
    ///      DO WHAT NEEDS TO BE DONE
    ///////////////////////////////////////////////////////////////////////

  finally
    cds1.Free;
  end;
end;

Я большой сторонник маленького кода (если он не слишком запутанный).

Если вы хотите пойти по этому (IMO) безобразному маршруту (групповая обработка с инициализацией nil, чтобы знать, требуется ли освобождение), вы по крайней мере ДОЛЖНЫ гарантировать, что вы не допустите, чтобы исключение в одном из деструкторов препятствовало освобождению остальных ваши объекты.
Что-то вроде:

function SafeFreeAndNil(AnObject: TObject): Boolean;
begin
  try
    FreeAndNil(AnObject);
    Result :=  True;
  except
    Result := False;
  end;
end;

var cds1  : TClientDataSet;
    cds2  : TClientDataSet;
    IsOK1 : Boolean;
    IsOK2 : Boolean;
begin
  cds1      := Nil;
  cds2      := Nil; 
 try
    cds1      := TClientDataSet.Create(nil);
    cds2      := TClientDataSet.Create(nil);    
    ///////////////////////////////////////////////////////////////////////
    ///      DO WHAT NEEDS TO BE DONE
    ///////////////////////////////////////////////////////////////////////
  finally
    IsOk2 := SafeFreeAndNil(cds2);    // an error in freeing cds2 won't stop execution
    IsOK1 := SafeFreeAndNil(Cds1);
    if not(IsOk1 and IsOk2) then
      raise EWhatever....
  end;
end;

Есть хорошее видео об исключениях в конструкторах и деструкторах

Он показывает несколько хороших примеров, таких как:

var cds1  : TClientDataSet;
    cds2  : TClientDataSet;
begin
  cds1      := Nil;
  cds2      := Nil; 
 try
    cds1      := TClientDataSet.Create(nil);
    cds2      := TClientDataSet.Create(nil);    
    ///////////////////////////////////////////////////////////////////////
    ///      DO WHAT NEEDS TO BE DONE
    ///////////////////////////////////////////////////////////////////////
  finally
    freeandnil(cds2);    //// what has if there in an error in the destructor of cds2
    freeandnil(Cds1);
  end;
end;

Что делать, если есть ошибка в деструкторе CDS2

Cds1 не будет уничтожен

РЕДАКТИРОВАТЬ

Еще один хороший ресурс:

Джим Маккит (Jim McKeeth) показал отличное видео об отложенной обработке исключений в диапазоне кода III, где он рассказывает о проблемах обработки исключений в блоке finally.

@mghie: Delphi получил объекты, выделенные из стека:

type
  TMyObject = object
  private
    FSomeField: PInteger;
  public
    constructor Init;
    destructor Done; override;
  end;

constructor TMyObject.Init;
begin
  inherited Init;
  New(FSomeField);
end;

destructor TMyObject.Done;
begin
  Dispose(FSomeField);
  inherited Done;
end;

var
  MyObject: TMyObject;

begin
  MyObject.Init;
  /// ...
end;

К сожалению, как показывает приведенный выше пример: объекты, размещенные в стеке, не предотвращают утечки памяти.

Так что это все равно потребует вызова деструктора, как это:

var
  MyObject: TMyObject;

begin
  MyObject.Init;
  try
    /// ...
  finally
    MyObject.Done;
  end;
end;

Хорошо, я признаю, что это почти не по теме, но я подумал, что это может быть интересно в этом контексте, поскольку объекты, выделенные из стека, упоминались как решение (а это не так, если нет автоматического вызова деструктора).

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