Объект Delphi создает внутри блока try?

В Delphi 7 создание объекта было следующим:

A := TTest.Create;
try
  ...
finally
  A.Free;
end;

Однако в блоге Марко Канти говорит, что в Эмберкадеро они используют

A1 := nil;
A2 := nil;
try
  A1 := TTest.Create;
  A2 := TTest.Create;
  ...
finally
  A2.Free;
  A1.Free;
end;

Было ли что-то изменено в логике попытки окончательно заблокировать во время обновления версии? Второй пример кажется типичной ошибкой для меня!

Ответ 1

Оба являются приемлемыми шаблонами. И это не то, что изменилось.

Сначала разрешите использовать тот, с которым вы знакомы, и почему он исправлен.

{ Note that here as a local variable, A may be non-nil, but
  still not refer to a valid object. }
A := TTest.Create;
try
  { Enter try/finally if and only if Create succeeds. }
finally
  { We are guaranteed that A was created. }
  A.Free;
end;

В приведенном выше: Если A был назначен после попытки, тогда существует вероятность того, что Create может выйти из строя и перейти сюда. Это попытается освободить объект из неопределенного места в памяти. Это может привести к нарушению доступа или нестабильному поведению. Обратите внимание, что компилятор также предупреждает, что на A.Free; что A может быть неинициализирован. Это связано с возможностью перехода к блоку finally до того, как A назначен из-за исключения в конструкторе.


Итак, почему код Marco является приемлемым?

A1 := nil; { Guarantees A1 initialised *before* try }
A2 := nil; { Guarantees A2 initialised *before* try }
try
  A1 := TTest.Create;
  A2 := TTest.Create;
  ...
finally
  { If either Create fails, A2 is guaranteed to be nil.
    And Free is safe from a nil reference. }
  A2.Free;
  { Similarly, if A1 Create fails, Free is still safe.
    And if A1 create succeeds, but A2 fails: A1 refers to a valid
    object and can be destroyed. }
  A1.Free;
end;

Обратите внимание, что код Марко опирается на некоторые тонкости поведения Free(). См. Следующие вопросы и ответы для получения дополнительной информации:


Цель этой методики заключается в том, чтобы избежать вложенных блоков try..finally, которые могут стать беспорядочными. Например

A1 := TTest.Create;
try
  A2 := TTest.Create;
  try
    {...}
  finally
    A2.Free;
  end;
finally
  A1.Free;
end;

Код Marco уменьшает уровни вложенности, но требует "предварительной инициализации" локальных ссылок.


Виктория подняла оговорку, что если деструктор для A2 не сработает в коде Марко, то A1 не будет Freed. Это будет некоторая утечка памяти. Однако я бы сказал, что как только любой деструктор потерпит неудачу:

  • он не завершился успешно;
  • так что, вероятно, уже утечка по крайней мере памяти или ресурсов;
  • а также общая целостность вашей системы подпадает под сомнение. Если "простая очистка" потерпела неудачу: почему, что пошло не так, какие будущие проблемы это вызовет?

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

Ответ 2

Есть одно важное дополнение к ответу и объяснению Крейга, почему использование одного блока try..finally также прекрасное.

A1 := nil;
A2 := nil;
try
  A1 := TTest.Create;
  A2 := TTest.Create;
  ...
finally
  A2.Free;
  A1.Free;
end;

Потенциальная проблема с приведенным выше кодом заключается в том, что если деструктор A2 поднимает или вызывает исключение, то A1 destructor не будет вызываться.

С этой точки зрения над кодом нарушается. Но все управление памятью Delphi построено на основе предпосылки, что деструкторы никогда не должны поднимать или вызывать исключение. Или, другими словами, если в деструкторе есть код, который может вызвать исключение, деструктор должен обработать это исключение на сайте и не позволить ему уйти.

В чем проблема с деструкторами, создающими исключения?

Вызов исключения в деструкторе приведет к нарушению вызова цепочки деструкторов. В зависимости от кода унаследованные деструкторы могут не вызываться, и они не смогут выполнить надлежащую очистку, что приведет к утечке памяти или ресурсов.

Но еще более важным фактом является то, что даже если у вас есть один деструктор, который вызывает необработанное исключение, метод FreeInstance который освобождает память экземпляра объекта, выделенную в куче, не будет вызываться, и вы пропустите эту память экземпляра объекта.

Это означает, что следующий код будет TTest память кучи памяти A.Free если A.Free содержит код, который вызовет исключение.

A := TTest.Create;
try
  ...
finally
  A.Free;
end;

То же самое верно для вложенных блоков try...finally. Если какой-либо из деструкторов вызывает необработанную память исключения, будет просочиться.

В то время как вложенные try...finally блокируют утечку меньше памяти, чем одна try...finally блоки, они все равно вызовут утечку.

A1 := TTest.Create;
try
  A2 := TTest.Create;
  try
    ...
  finally
    A2.Free;
  end;
finally
  A1.Free;
end;

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


Как насчет BeforeDestruction?

То же правило, которое применяется к деструкторам, применяется к методу BeforeDestruction. Необработанное исключение в BeforeDestruction нарушит процесс освобождения объекта и цепочку деструкторов, а FreeInstance не будет вызван, что приведет к утечке памяти.


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


Мы можем с уверенностью утверждать, насколько нарушен какой-то код, так как он сломан. Все приведенные выше примеры вызовут утечку памяти, если какой-либо из деструкторов вызывает необработанное исключение. И единственный способ, которым такой код может быть правильно исправлен, - это исправление сломанных деструкторов.


Что именно обрабатывает исключения?

Обработка исключений выполняется в try...except block. Любое исключение, которое попадает в этот блок и не ре-рейзируется, обрабатывается. С другой стороны, try...finally блоки используются для очистки (выполнение кода, которое должно обязательно выполняться даже в случае исключения), а не для обработки исключений.

Например, если у вас есть код в BeforeDestruction или деструктор, выполняющий строку до целочисленного преобразования, код может поднять EConvertError. Вы можете поймать это исключение с помощью try...except block и обработать его там, не позволяя ему уйти и вызвать хаос.

destructor TFoo.Destroy;
var
  x: integer;
begin
  try
    x := StrToInt('');
  except
    on E: EConvertError do writeln(E.ClassName + ' handled');
  end;
  inherited;
end;

Если есть какой-то код очистки, который вы должны выполнить, вы также можете использовать try... наконец, блокировать внутри и следить за тем, чтобы какой-либо код очистки выполнялся должным образом.

destructor TFoo.Destroy;
var
  x: integer;
begin
  try
    try
      x := StrToInt('');
    finally
      writeln('cleanup');
    end;
  except
    on E: EConvertError do writeln(E.ClassName + ' handled');
  end;
  inherited;
end;

Другой способ обработки исключений - это предотвращение их в первую очередь. Прекрасным примером является вызов Free во внутренних полях вместо вызова Destroy. Таким образом, деструкторы могут обрабатывать частично сконструированные экземпляры и выполнять надлежащую очистку. Если FBar равен нулю FBar.Free ничего не сделает, но FBar.Destroy вызовет исключение.

destructor TFoo.Destroy;
begin
  FBar.Free;
  inherited;
end;

Как не обрабатывать исключения во время процесса уничтожения

Не ходите вокруг написания try...except блоков в каждом деструкторе, который вы когда-либо писали. Не каждая строка кода может вызвать исключение, и абсолютно все исключения повсюду должны быть съедены.

Исключения - это исключительные события, которые могут возникать в некоторых кодах при определенных обстоятельствах, но это не означает, что вы не можете распознать код, который может вызвать исключения и защитить его.

Кроме того, завершение всего кода с помощью try...except block не позволит вам оставаться в безопасности. Вы должны иметь дело с исключениями в каждом деструкторе.

Например, если деструктор FBar может вызвать исключение, вы должны обработать это исключение в деструкторе TBar. TFoo в обработчик исключений внутри TFoo приведет к утечке экземпляра FBar поскольку его деструктор ошибочен, и он не выпустит FBar кучи FBar.

destructor TFoo.Destroy;
begin
  // WRONG AS THIS LEAKS FBar instance
  try
    FBar.Free;
  except
    ...
  end;
  inherited;
end;

Это правильная обработка исключения, которое может быть поднято в деструкторе TBar

destructor TBar.Destroy;
begin
  try
    // code that can raise an exception
  except
    ...
  end;
  inherited;
end;

destructor TFoo.Destroy;
begin
  FBar.Free;
  inherited;
end;