Лучшая практика для выполнения вложенного оператора 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;

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


person Charles Faiga    schedule 29.12.2008    source источник
comment
Прежде всего: не передавайте в Create значение Владельца, отличное от nil, если вы все равно собираетесь освобождать объекты самостоятельно. Это просто добавляет много ненужных накладных расходов.   -  person Oliver Giesen    schedule 31.12.2008
comment
Ааааа поездка по переулку памяти.   -  person Dave Van den Eynde    schedule 05.02.2009
comment
Я знаю, опоздал на 6 месяцев, но технически я думаю, что это правильный и безопасный способ. Все предложенные ответы имеют возможные утечки или требуют подпрограмм резервного копирования / поддержки. Этот код будет работать без утечек.   -  person Despatcher    schedule 20.06.2009
comment
imho нет лучшего способа - не следует использовать только конструктор, отличный от nil.   -  person mjn    schedule 25.03.2010


Ответы (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;

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

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

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

РЕДАКТИРОВАТЬ изменил создание объекта так, чтобы он не ссылался на приложение, поскольку в этом примере это не обязательно.

person skamradt    schedule 29.12.2008
comment
Вам необходимо инициализировать cds2, cds3 и cds4 с помощью nil, иначе вы получите AV при освобождении (недействительных) объектов, если создание cds1 не удалось. Только строки и указатели интерфейсов могут считаться пустыми при использовании в качестве переменных стека. - person mghie; 29.12.2008
comment
Вам не нужны проверки if Assigned (cdsX), FWIW. - person utku_karatas; 29.12.2008
comment
Также обратите внимание, что этот код уязвим для утечек памяти, если TClientDataSet.Destroy () вызывает исключения. Посмотрите мой собственный ответ на код, который также можно написать, чтобы обезопасить себя от этого. - person mghie; 29.12.2008
comment
Это действительно ужасно! Довольно сложно уйти от лучших практик: - person Oliver Giesen; 31.12.2008
comment
1. Create всегда нужно размещать перед try / finally-free в случае исключений в конструкторе - инициализация до nil для предотвращения этого - всего лишь уродливый взлом. 2. вы либо передаете Владельца, отличного от nil, для создания, либо освобождаете объект самостоятельно, но не сразу и то, и другое! - person Oliver Giesen; 31.12.2008
comment
@Oliver: Опять же, передача владельца, отличного от nil, при создании компонентов не предотвращает ручное удаление - VCL обрабатывает этот случай без каких-либо проблем. Для доказательства см. Источники TComponent.Destroy () и TComponent.Notification (). - person mghie; 31.12.2008
comment
@Oliver суть заключалась в том, что если в одном из созданных объектов возникает исключение, автоматически вызывается освобождение любого из подобъектов и устраняется вложение. - person skamradt; 31.12.2008
comment
@mghie: Это действительно не мешает этому, так что на самом деле это не неправильно, но весь механизм управления владельцем добавляет много совершенно ненужных накладных расходов. - person Oliver Giesen; 31.12.2008
comment
Единственное, что я бы, наверное, изменил, - это ход 1 создания из Try ... Наконец. Не то чтобы это имеет большое значение, но все же немного более оптимально. Что касается возможных исключений в деструкторах. Сама VCL вряд ли этим заморачивается. Я только что проверил TForm.Destroy (вариант D2010), и он не беспокоит упаковку каждого бесплатного в try..except. Итак, что касается VCL, исключение в деструкторе кажется критическим отказом. - person Ken Bourassa; 02.09.2010
comment
Я справляюсь с этим, используя мою собственную процедуру InitialiseNil, которая принимает несколько параметров, и процедуру FreeAndNil, которая также принимает несколько параметров. Это немного улучшает чтение приведенного выше кода. - person David Heffernan; 07.01.2011
comment
Это не хорошо. Если в одном из первых трех освобождений возникает исключение, происходит утечка памяти. - person Pieter B; 31.07.2012
comment
@DavidHeffernan, не могли бы вы рассказать больше об этих подпрограммах InitializeNil / FreeAndNil? Единственное решение, с которым я пришел, - это InitializeNil (objs: array of PObject) и вызывать его с помощью InitializeNil ([@ obj1, @ obj2, ...]), но это выглядит не очень хорошо. - person Yuriy Afanasenkov; 15.05.2016
comment
@Yuri: просто имейте несколько перегруженных версий InitializeNil, одну с одним параметром, одну с двумя и т. Д. В любом случае вам, вероятно, не понадобится больше трех или четырех параметров. - person Rudy Velthuis; 03.09.2017

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

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:

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

person mghie    schedule 29.12.2008
comment
Хорошая реализация, +1 для использования, но она, наконец, полностью исключает попытку, что может быть достаточно хорошим для этого примера. - person skamradt; 29.12.2008
comment
Я вижу попытку ... наконец, это не более чем костыль, необходимый, потому что RAII невозможен в Delphi без интерфейсов. Если бы в Delphi были объекты, размещенные в стеке, не было бы особой нужды пытаться ... наконец. Тем не менее, его можно использовать вместе с интерфейсом SafeGuard. - person mghie; 29.12.2008
comment
@mghie: За исключением случаев, когда деструктор закрывает дескриптор или освобождает некоторые другие ресурсы, что довольно часто. - person himself; 20.10.2010
comment
@ себя: я не слежу. Деструкторы никогда не должны генерировать исключения, поскольку управление владением компонентами VCL небезопасно для исключений. Итак, если будут вызваны все деструкторы, что вы имеете в виду, кроме случаев, когда ...? - person mghie; 20.10.2010
comment
@mghie: Если их назовут, то да. Думал, вы говорите о том, чтобы просто раскрутить стек в случае чего. Кстати, деструкторы никогда не могут генерировать исключения. Вы не всегда знаете, когда будет сгенерировано исключение, в конце концов, это исключение. - person himself; 21.10.2010
comment
@himself: Если вы перейдете по ссылке на статью, то увидите, что деструкторы будут вызываться. И хотя вы правы в том, что деструкторы могут генерировать исключения, им лучше этого не делать (по крайней мере, в TComponent потомках, которые используются с управлением временем жизни VCL). Утечки памяти и / или сбои являются неизбежным следствием. - person mghie; 21.10.2010

Есть еще один вариант кода без вложенной попытки ... наконец, это только что пришло мне в голову. Если вы не создаете компоненты с параметром 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;

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

person mghie    schedule 30.12.2008
comment
Не совсем так. На самом деле не имеет значения, есть ли у компонента, который я освобождаю вручную, владельца или нет - разрушение объекта запускает Notification () для владельца, поэтому он удаляет отправителя из списка принадлежащих ему объектов. Но я согласен, передача nil, поскольку AOwner работает так же хорошо. - person mghie; 31.12.2008
comment
Однако согласился со вторым комментарием. Не освобождать cds1 (т.е. позволить приложению делать это при завершении работы приложения) немного лучше, чем утечка памяти. - person mghie; 31.12.2008
comment
Хорошо, но IObjectSafe лучше IMO, потому что он обрабатывает TObjects, а также TComponents. - person Roddy; 06.01.2009
comment
@Roddy: Полностью согласен. С такими интерфейсами можно сделать гораздо больше, я начал использовать их постоянно (например, для установки и сброса курсоров занятости, блокировки и разблокировки объектов синхронизации, временного отключения обновлений и многого другого). Отличный материал. - person mghie; 07.01.2009

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

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;
person user81126    schedule 23.03.2009

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

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

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 не будет уничтожен

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

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

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

person Charles Faiga    schedule 10.01.2009
comment
Я уже прокомментировал этот вопрос по принятому вами ответу. Если вы хотите избавиться от вложенных блоков finally, используйте вспомогательный объект, который освобождает все охраняемые объекты. Затем это можно сделать безопасным способом. Обратите внимание, что у TComponent.DestroyObjects () такая же проблема! - person mghie; 10.01.2009
comment
Я только что попробовал, и действительно, создание исключения в деструкторе компонента, принадлежащего вторичной форме, действительно приводит к нарушениям доступа при закрытии приложения. Поэтому, вероятно, лучше всего попытаться исключить все возможности для исключений в деструкторах. - person mghie; 10.01.2009
comment
@mghie: Ознакомьтесь с обработкой отложенных исключений, на которую он ссылается. Я написал его специально для обработки исключений в деструкторах, поскольку, когда код является сторонним, вы не всегда можете его изменить. - person Jim McKeeth; 28.10.2010
comment
@Jim: Верно, и VCL не исключение, это третья сторона, которую вы не можете изменить. Я не знаю о последней версии VCL, но, поскольку управление владением VCL исторически никогда не было безопасным для исключений, было бы не лучшим решением, чтобы деструкторы компонентов генерировали исключения. Если компоненты принадлежат форме и освобождены VCL, это приведет к утечкам памяти и / или сбоям. Ваш код также не помогает с собственными компонентами. - person mghie; 28.10.2010

@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;

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

person dummzeuch    schedule 29.12.2008
comment
Извините за то, что не совсем ясно. Автоматический вызов деструктора - это именно то, что мне нужно, независимо от того, выделен ли экземпляр в стек и выходит за пределы области видимости, или содержится ли он в другом объекте, а деструктор содержащихся объектов вызывается при уничтожении родителя. RAII, как это делает C ++ ... - person mghie; 29.12.2008
comment
Не используйте предметы старого стиля. Они плохо работают с наследованием и не поддерживают какие-либо типы Delphi, представленные в последнее десятилетие, такие как длинные строки, интерфейсы или динамические массивы. - person Rob Kennedy; 31.12.2008
comment
@ Роб: Что вы имеете в виду под старыми стильными предметами? TObject? Какая альтернатива лучшая? - person lukeck; 02.01.2009
comment
@Luke: объект старого стиля создается с помощью TMyObject = object вместо TMyObject = class или даже TMyObject = class (TObject). Объекты старого стиля давно устарели и существуют только для обратной совместимости. - person mghie; 02.01.2009