克雷格的回答和解释有一个重要补充,为什么使用单个 try..finally 块也可以。
A1 := nil;
A2 := nil;
try
A1 := TTest.Create;
A2 := TTest.Create;
...
finally
A2.Free;
A1.Free;
end;
上述代码的潜在问题是如果A2析构函数引发或导致异常A1析构函数将不会被调用。
从这个角度来看,上面的代码被破坏了。但是,整个 Delphi 内存管理是建立在析构函数永远不应该引发或导致异常的前提之上的。或者换句话说,如果析构函数中有代码会导致异常,则析构函数必须在现场处理该异常并且不允许它逃逸。
析构函数引发异常有什么问题?
在析构函数中引发异常将破坏调用析构函数链。根据代码,继承的析构函数可能不会被调用,它们将无法执行适当的清理,从而导致内存或资源泄漏。
但更重要的事实是,即使您有一个导致未处理异常的析构函数,也不会调用释放在堆上分配的对象实例内存的FreeInstance 方法,并且您将泄漏该对象实例的内存。
这意味着如果A.Free 包含将导致异常的代码,则以下代码将泄漏TTest 实例堆内存。
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 块内完成。处理该块捕获且未重新引发的任何异常。另一方面,try...finally 块用于清理(执行即使在异常情况下也必须运行的代码),而不是用于处理异常。
例如,如果您在BeforeDestruction 中有一些代码或析构函数进行字符串到整数的转换,则该代码可以引发EConvertError。您可以使用 try...except 块捕获该异常并在那里处理它,而不是让它逃逸并造成破坏。
destructor TFoo.Destroy;
var
x: integer;
begin
try
x := StrToInt('');
except
on E: EConvertError do writeln(E.ClassName + ' handled');
end;
inherited;
end;
如果有一些你必须执行的清理代码,你也可以在里面使用 try...finally 块并确保任何清理代码都能正确执行。
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 为nil,FBar.Free 将什么也不做,但FBar.Destroy 会引发异常。
destructor TFoo.Destroy;
begin
FBar.Free;
inherited;
end;
销毁过程中如何不处理异常
不要在你写过的每个析构函数中到处写try...except 块。不是每一行代码都会导致异常,也不是绝对所有的异常都应该被吃掉。
异常是某些代码在特定情况下可能发生的异常事件,但这并不意味着您无法识别可能导致异常的代码并对其进行保护。
另外,用try...except 块包裹所有代码不会保证你的安全。您必须在每个析构函数中处理异常。
例如,如果FBar 析构函数会导致异常,那么您必须在TBar 析构函数中处理该异常。在TFoo 析构函数内的异常处理程序中将其包装起来会泄漏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;