在Delphi 7中,对象的创建是这样的:
A := TTest.Create;
try
...
finally
A.Free;
end;
然而在blog articleMarcoCantù说在Embercadero他们使用
A1 := nil;
A2 := nil;
try
A1 := TTest.Create;
A2 := TTest.Create;
...
finally
A2.Free;
A1.Free;
end;
在版本升级期间,try finally逻辑中的内容是否发生了变化?第二个例子对我来说似乎是一个典型的错误!
答案 0 :(得分:25)
Both are acceptable patterns. And this is not something that's changed.
First let's cover the one you're familiar with and why it's correct.
{ 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;
In the above: If A had been assigned after try, then there's a possibility that Create could fail and jump here. This would attempt to Free an object from an undefined location in memory. It can lead to an Access Violation or unstable behaviour. Note that the compiler would also give a warning that on A.Free;
that A
might be uninitialised. This is because of the possibility of jumping to the finally block before A
is assigned due to an exception in the constructor.
So why is Marco's code acceptable?
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's Create fails, Free is still safe.
And if A1's create succeeds, but A2's fails: A1 refers to a valid
object and can be destroyed. }
A1.Free;
end;
Note that Marco's code relies on some subtleties of the behaviour of Free()
. See the following Q&A for more information:
The purpose behind the technique is to avoid nested try..finally blocks which can get messy. E.g.
A1 := TTest.Create;
try
A2 := TTest.Create;
try
{...}
finally
A2.Free;
end;
finally
A1.Free;
end;
Marco's code reduces nesting levels, but requires 'pre-initialisation' of the local references.
Victoria has raised a caveat that if the destructor for A2
fails in Marco's code, then A1
will not be Freed. This would be a certain memory leak. However, I'd argue that as soon as any destructor fails:
So the best advice I can offer is: take care to ensure the correctness of your destructors.
答案 1 :(得分:15)
Craig的答案和解释有一个重要的补充,为什么使用单try..finally
块也没问题。
A1 := nil;
A2 := nil;
try
A1 := TTest.Create;
A2 := TTest.Create;
...
finally
A2.Free;
A1.Free;
end;
上面代码的潜在问题是,如果A2
析构函数引发或导致异常A1
,则不会调用析构函数。
从上述观点来看,代码被打破了。但是,整个Delphi内存管理建立在析构函数永远不会引发或导致异常的前提之上。或者换句话说,如果析构函数中存在可能导致异常的代码,则析构函数必须在站点上处理该异常并且不允许它转义。
析构函数引发异常有什么问题?
在析构函数中引发异常会破坏调用析构函数链。根据代码,可能无法调用继承的析构函数,并且它们将无法执行正确的清理,从而导致内存或资源泄漏。
但更重要的事实是,即使您有一个导致未处理异常的析构函数,也不会调用释放堆上分配的对象实例内存的FreeInstance
方法,并且您将泄漏该对象实例存储器中。
这意味着如果TTest
包含将导致异常的代码,则以下代码将泄漏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
块内完成的。处理该块捕获但未重新引发的任何异常。另一方面,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
为零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;