Было ли что-то изменено в логике попытки окончательно заблокировать во время обновления версии? Второй пример кажется типичной ошибкой для меня!
Ответ 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;