Delphi异常处理 - 如何正确清理?

10

我在查看我们应用程序中的一些代码时,发现了一些与我通常做的有所不同。在异常处理和清理方面,我们(以及许多其他程序员,我相信)使用嵌套在Try/Finally块内部的Try/Except块。现在,我习惯于这样在Try/Finally内部使用Try/Except:

Try
  Try
    CouldCauseError(X);
  Except
    HandleError;
  end;
Finally
  FreeAndNil(x);
end;

但是这个代码块是反转的:

Try
  Try
    CouldCauseError(X);
  Finally
    FreeAndNil(x);
  end;
Except
  HandleError;
end;

在网上看到一些人这样做,但没有解释为什么。我的问题是,哪种方式放在外部块,哪种方式放在内部块是否有影响?或者无论如何结构化,except和finally部分都会被处理吗?谢谢。

4个回答

5

一个不同之处是try..finally..except可能会潜在地存在异常掩盖情况。

想象一下,在CouldCauseError()中发生了一个异常。然后再想象一下,在finally中尝试FreeAndNIL(X)时,又引发了另一个异常。原始异常(很可能导致FreeAndNIL()异常不稳定的异常)就丢失了。现在except处理程序正在处理原始异常之后发生的“下游”异常。

当然,try..except..finally避免了这种情况,并且应该优先选择(尽可能靠近源头处理异常)。

处理这种简单情况的另一种方法(清理单个对象)是将清理包括在正常流程和异常处理程序中:

try
  CouldCauseError(X);
  FreeAndNil(x);
except
  HandleError;
  FreeAndNil(x);
end;

一开始看起来有些可怕(“我需要确保调用 FreeAndNIL(X),所以我必须要有一个 FINALLY!!”),但是第一个 FreeAndNIL() 不会被调用的唯一情况就是发生异常,如果确实发生了异常,你也会进行 FreeAndNIL(),这使得在异常清理的顺序上变得更加清晰,去除了一些噪声,以一定程度上帮助理解正在发生的事情。
但是,个人而言,我不太喜欢这种方式——如果你将异常处理程序或正常流程中的代码更改,你可能会破坏清理行为,但是根据周围代码和块本身的大小,减少“噪声”的效果有时可以被认为是有道理的,为简化而努力。
然而,这取决于 FreeAndNIL() 是否真的是“NILThenFree()”...在正常流程的 FreeAndNIL(X) 中如果发生异常,则 X 在异常处理程序捕获由 X.Free 引发的异常时将被 NIL,因此它不会尝试“二次释放” X。
无论你做出什么决定,希望这能有所帮助。

Delphi的主要前提是析构函数绝不能引发异常。如果FreeAndNil可能会引发异常,那么整个代码都将被破坏(无论如何进行清理),并且由于对象实例的销毁被中断而未能正确完成,因此将导致内存泄漏。 - Dalija Prasnikar
FreeAndNIL()的编写不会引发异常,因此永远不会这样做。但是,即使析构函数(由FreeAndNil()调用)没有故意或明确地引发异常,在异常情况下仍可能无意中引发异常(也许是您正在使用的一些糟糕编写的库代码,该代码不遵循“规则”)。 话虽如此,您提出的担忧正是我建议使用try..except..finally的原因,因为该模式最可靠地暴露了由于FreeAndNIL引发异常的情况(如果使用try..finally..except,则不太清楚)。 - Deltics
我留下评论并不是因为我想就你在答案中的建议进行争论,而是作为对未来读者的额外观察。如果析构函数链被打破,即使看起来所有异常都被正确捕获和处理,仍然会出现内存泄漏。许多开发人员并没有完全意识到这个问题。 - Dalija Prasnikar
1
没关系 - 我并不是在反驳你,只是强调最佳实践和完美的代码卫生假设是不能依赖的,事实上这进一步加强了使用try..except..finally建议的推荐,以便更轻松地识别此类问题何时何地已经渗入到代码库中。 :) - Deltics

4

finally和except都会被触发,它们的顺序由你决定。这取决于你在finally或except块中想要做什么。如果你想释放在except块中使用的某些内容,请将finally放置在except块周围。


2

一切取决于finally块中的代码是否会自己引发异常(这时需要由上层try except保护),或者您是否需要在异常处理中释放以后的资源(这时需要在上层finally块中释放)。这意味着有时候您甚至可以像这样编写代码:

  try
    try
      try
        CouldCauseError(X);
      except
        HandleErrorWith(X);
      end;
    finally
      FreeAndNil(X); // and/or any resource cleanup
    end;
  except
    CatchAllError;
  end;

0

一开始你的代码看起来有点奇怪,我错过了创建X的部分。

X := CreateAnX
try
  DoSomeThing(X);
finally
  FreeAndNil(x);
end;

这很重要。因为如果你有像这样的代码

// don't do something like this
try
  X := CreateAnX
  DoSomeThing(X);
finally
  FreeAndNil(x);
end;

你可以运气好,它能够正常工作。但如果构建失败了,你可能会“幸运”地遭遇访问冲突,或者你就是倒霉,在完全不同的代码位置有时会发生访问违规。

另一种选择是

X := nil;
try
  X := CreateAnX
  DoSomeThing(X);
finally
  FreeAndNil(x);
end;

在编程中使用 except 取决于你的意图。当你想捕获每一个异常并清除所有调用代码的问题(使用 try finally),那么外层 except 块就是正确的选择。

try
  X := CreateAnX
  try
    DoSomeThing(X);
  finally
    FreeAndNil(x);
  end;
except
  on e: Exception do
    LogException(e)
end;      

但是在想要捕获所有错误时,一定要考虑清楚。例如(我经常看到这种错误),不要在Indy OnExecute处理程序中这样做。你必须使用类似这样的东西

try
  X := CreateAnX
  try
    DoSomeThing(X);
  finally
    FreeAndNil(x);
  end;
except
  on EIdException do
    raise;
  on e: Exception do
    LogException(e)
end;      

如果你预期会有异常,因为你抛出了它或者(比如)一个对话可能会失败,那么请尝试找到最内层的位置去捕获这个错误:
X := CreateAnX
try
  DoSomeThing(X);
  try
    i := StrToInt(X.Text);
  except
    on EConvertError do
      i := 0;
  end;       
finally
  FreeAndNil(x);
end;

不要这样做

X := CreateAnX
try
  try
    DoSomeThing(X);
    i := StrToInt(X.Text);
  except
    on EConvertError do
      i := 0;
  end;       
finally
  FreeAndNil(x);
end;

只有在你也预期DoSomeThing会抛出EConvertError时才这样做。如果DoSomeThing抛出了EConvertError而你并没有预期,那么你的代码存在严重问题,需要进行修正。在这种情况下,请确保用户可以保存他的工作(可能是作为一个备份,因为他的工作可能被损坏),并确保你获取到有关问题的信息。
至少使用try except来处理异常,而不是隐藏它们。

消除crete语句是有意的,因为我假设(这是个错误的决定),它会像begin和end语句一样被认为是与问题无关的。是的,在第一个try语句之前我们总是创建X。 - Tom A

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接