【问题标题】:Delphi object create inside try block?Delphi对象在try块内创建?
【发布时间】:2018-07-15 07:03:14
【问题描述】:

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

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

然而在blog articleMarco Cantù 中说他们在 Embercadero 使用

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

在版本升级期间,try finally 块的逻辑是否发生了变化?第二个例子对我来说似乎是一个典型的错误!

【问题讨论】:

  • 在第二个示例中,您创建了 两个 对象,而不是一个!不过,在这种情况下,我更喜欢嵌套的 try..finally 块。
  • 我知道这不应该发生,但如果析构函数A2.Free; 引发异常,则可能会发生泄漏。我支持选项 1,嵌套块。
  • 有趣的是,Marco 说他们将使用 OP 提供的代码作为他们的类库代码。 耸耸肩
  • @David,如果A2 析构函数引发异常,它不仅会泄漏自身,还会泄漏A1,因为它的析构函数不会被调用。嵌套块将至少解决这个问题。而且我不是说修复。这是我比较喜欢的编码方式。析构函数永远不应该引发异常。这是最重要的。
  • @Victoria 现在你的代码很丑,但仍然会泄漏。如果你不能解决所有问题,最好有干净的代码,而不是修复一半的问题并有不可读的丑陋代码。而且这个问题远比简单的堆内存泄漏要大。资源、文件句柄等不会返回系统。导致相应的故障、数据损坏和天知道是什么。如果析构函数无法完成,最好的办法是强制终止进程。

标签: delphi


【解决方案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;

在上面:如果在 try 之后分配了 A,那么 Create 可能会失败并跳转到这里。这将尝试从内存中未定义的位置释放对象。它可能导致访问冲突或不稳定的行为。 请注意,编译器还会在 A.Free; 上发出警告,指出 A 可能未初始化。这是因为可能会跳转到 finally 块之前 em> 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;

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


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

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

Marco 的代码减少了嵌套级别,但需要对本地引用进行“预初始化”。


Victoria 提出了一个警告,如果 A2 的析构函数在 Marco 的代码中失败,那么 A1 将不会被释放。这将是一定的内存泄漏。但是,我认为一旦任何析构函数失败:

  • 尚未成功完成;
  • 所以可能至少已经泄漏了内存或资源;
  • 而且,您的系统的整体完整性受到质疑。 如果“简单清理”失败:为什么,出了什么问题,这将导致什么未来问题?

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

【讨论】:

  • 正如 Victoria 所指出的,唯一需要注意的是,您的析构函数可能永远不会抛出 AV,否则您将有泄漏,也许将其添加到您的答案中......
  • @whosrdaddy 我同意这是一个警告;但是如果析构函数失败了,你可能有更重要的事情要担心。确保析构函数只关注单一的销毁任务是一个很好的理由。
  • 最佳实践是假设析构函数从不引发异常。由于您无法实际处理这种情况,请确保它永远不会发生,并相应地编写代码。
  • 析构函数本身可能不会引发异常,但是析构函数有可能导致异常,这就是为什么要始终谨慎使用方案 2 和 3 的原因跨度>
  • @DaveNottage 我不确定你所说的“析构函数本身可能不会引发异常”是什么意思。 raise <exception>; 是在 destructor 内部调用还是在它调用的方法中调用几乎无关紧要。 在析构函数的上下文中仍然是一个例外。在这方面:任何异常,在任何析构函数(上下文)中,在 any try..finally 模式 中意味着 1 个或多个破坏步骤 failed。因此,一些清理将失败。意味着某些资源(内存、句柄、锁等)未被释放。没有try..finally可以保证绝对安全。
【解决方案2】:

克雷格的回答和解释有一个重要补充,为什么使用单个 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;

【讨论】:

  • 根据我在另一个答案中的评论,析构函数可能导致异常(而不是引发异常)。这种情况满足这种情况
  • @DaveNottage 就内存管理而言,引起或引发异常基本相同。我将编辑答案以使其更清楚。在任何情况下,异常都不应逃脱析构函数。无论您在这种情况下编写什么样的代码,您总是会产生泄漏。
  • 我知道这是一回事:我的观点是关于什么是“安全”或不“安全”。这种情况不如嵌套块安全
  • @DaveNottage 如果它坏了有多大关系?
  • @DaveNottage 不,析构函数可以引发异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
  • 2013-09-01
  • 2021-11-25
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多