在try块中创建Delphi对象?

Cho*_*pin 12 delphi

在Delphi 7中,对象的创建是这样的:

A := TTest.Create;
try
  ...
finally
  A.Free;
end;
Run Code Online (Sandbox Code Playgroud)

然而在一篇博客文章中,MarcoCantù说他们在Embercadero中使用

A1 := nil;
A2 := nil;
try
  A1 := TTest.Create;
  A2 := TTest.Create;
  ...
finally
  A2.Free;
  A1.Free;
end;
Run Code Online (Sandbox Code Playgroud)

在版本升级期间尝试最终阻止的逻辑中是否发生了变化?第二个例子对我来说似乎是一个典型的错误!

Dis*_*ned 25

两者都是可接受的模式 不是改变的事情.

首先让我们介绍一下你熟悉的那个以及它为什么是正确的.

{ 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;
Run Code Online (Sandbox Code Playgroud)

在上面:如果在尝试之后分配了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'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;
Run Code Online (Sandbox Code Playgroud)

请注意,Marco的代码依赖于行为的一些微妙之处Free().有关更多信息,请参阅以下问答:


该技术背后的目的是避免嵌套的try..finally块可能会变得混乱.例如

A1 := TTest.Create;
try
  A2 := TTest.Create;
  try
    {...}
  finally
    A2.Free;
  end;
finally
  A1.Free;
end;
Run Code Online (Sandbox Code Playgroud)

Marco的代码降低了嵌套级别,但需要对本地引用进行"预初始化".


维多利亚提出了一个警告,即如果破坏者A2在Marco的代码中失败,那么A1就不会被释放.这将是一定的内存泄漏.但是,我认为只要任何析构函数失败:

  • 它没有成功完成;
  • 所以可能已经泄漏了至少内存或资源;
  • 而且,系统的整体完整性也存在疑问.如果"简单清理"失败了:为什么,出了什么问题,未来会出现什么问题呢?

所以我能提供的最佳建议是:注意确保析构函数的正确性.

  • 正如维多利亚指出的那样,唯一需要注意的是你的析构函数可能永远不会抛出AV,否则你会发生泄漏,或许可以将其添加到你的答案中...... (7认同)
  • @whosrdaddy我同意这是一个警告; 但如果析构函数失败,您可能需要担心更重要的事情.确保析构者只关心破坏的单一任务是一个很好的理由. (3认同)
  • 最佳实践是假设析构函数从不引发异常.由于您无法真实地处理该场景,因此请确保它永远不会发生,并相应地进行编码. (2认同)

Dal*_*kar 15

克雷格的答案和解释有一个重要的补充,为什么使用单try..finally一块也很好.

A1 := nil;
A2 := nil;
try
  A1 := TTest.Create;
  A2 := TTest.Create;
  ...
finally
  A2.Free;
  A1.Free;
end;
Run Code Online (Sandbox Code Playgroud)

上面代码的潜在问题是,如果A2析构函数引发或导致异常,A1则不会调用析构函数.

从上述观点来看,代码被打破了.但是,整个Delphi内存管理建立在析构函数永远不会引发或导致异常的前提之上.或者换句话说,如果析构函数中存在可能导致异常的代码,则析构函数必须在站点上处理该异常并且不允许它转义.

析构函数引发异常有什么问题?

在析构函数中引发异常会破坏调用析构函数链.根据代码,可能不会调用继承的析构函数,并且它们将无法执行正确的清理,从而导致内存或资源泄漏.

但更重要的事实是,即使您有一个导致未处理异常的析构函数,FreeInstance也不会调用释放在堆上分配的对象实例内存的方法,并且您将泄漏该对象实例的内存.

这意味着TTest如果A.Free包含将导致异常的代码,以下代码将泄漏实例堆内存.

A := TTest.Create;
try
  ...
finally
  A.Free;
end;
Run Code Online (Sandbox Code Playgroud)

这对嵌套try...finally块有效.如果任何析构函数导致未处理的异常内存将被泄露.

虽然嵌套try...finally块会比单个try...finally块泄漏更少的内存,但它们仍然会导致泄漏.

A1 := TTest.Create;
try
  A2 := TTest.Create;
  try
    ...
  finally
    A2.Free;
  end;
finally
  A1.Free;
end;
Run Code Online (Sandbox Code Playgroud)

您可以根据需要使用任意数量的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;
Run Code Online (Sandbox Code Playgroud)

如果你必须执行一些清理代码,你也可以使用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;
Run Code Online (Sandbox Code Playgroud)

另一种处理异常的方法 - 首先是阻止它们.完美的例子是调用Free内部字段而不是调用Destroy.这样,析构函数可以处理部分构造的实例并执行适当的清理.如果FBar是零FBar.Free将无能为力,但FBar.Destroy会引发异常.

destructor TFoo.Destroy;
begin
  FBar.Free;
  inherited;
end;
Run Code Online (Sandbox Code Playgroud)

如何在销毁过程中不处理异常

不要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;
Run Code Online (Sandbox Code Playgroud)

这是对TBar析构函数中可能引发的异常的正确处理

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

destructor TFoo.Destroy;
begin
  FBar.Free;
  inherited;
end;
Run Code Online (Sandbox Code Playgroud)

  • @DaveNottage如果它被破坏了多少呢? (2认同)