你们的异常处理策略遵循哪些原则?

27

在处理异常时,涉及到很多相对性。 除了低级API覆盖从硬件和操作系统引发的错误的情况外,还存在一个模糊不清的领域,在这个领域中,程序员决定什么是异常,什么是正常条件。

你如何决定何时使用异常?是否有关于异常的一致政策?


1
如果您需要一个.NET库来应用策略,请考虑使用Polly - Michael Freidgeim
12个回答

30

在对象内部的方法之间传递信息时,不应使用异常作为一种方法,而应该使用错误代码和防御性编程来处理本地情况。

异常的设计目的是将控制从检测到错误的点转移到可以处理错误的地方(堆栈更高的位置),假设由于局部代码没有足够的上下文来纠正问题,因此堆栈更高的位置将拥有更多的上下文,并能够更好地组织恢复。

在考虑异常时(至少在C++中),应考虑您的API提供的异常保证。最低级别的保证应该是基本保证,尽管在适当的情况下,应努力提供强保证。在您不使用特定API的任何外部依赖项的情况下,甚至可以尝试提供无抛出保证。

注意:不要将异常保证与异常规范混淆。

异常保证:

无保证:

在异常发生后,无法保证对象的状态。 在这些情况下,不应再使用该对象。

基本保证:

几乎在所有情况下,这应该是方法提供的最低保证。 这保证了对象的状态是定义良好的,并且仍然可以一致地使用。

强保证(也称事务性保证):

这保证了方法将完全成功执行, 或者抛出异常并且对象的状态不会更改。

无抛出保证:

该方法保证不允许异常传播到方法外部。 所有析构函数都应该提供此保证。
| 注意:如果一个异常在已经传播异常时从析构函数中逃逸
| 应用程序将终止。


很好的东西。如果我提出问题,它会更抽象和正式一些,但是我投了赞成票。 - Dustman
@Martin York:+1 很好的内容;如果您包括“请不要捕获任何异常,请”,我会给予+2。 - user7116
你可以让它更清晰,说明那些保证是什么。 - iny
@iny:你是什么意思?语言中没有明确定义保证的内容。这完全取决于你编写/记录代码的方式。 - Martin York

7
这篇来自微软高级软件设计工程师Eric Lippert的博客总结了一组优秀且简要的异常处理策略指南
简而言之:
  • 致命错误:这些错误表示您的进程已经无法恢复。清理您可以的所有资源,但不要捕获它们。如果您编写的代码可以检测到这种情况,请将其抛出。例如:内存不足异常。

  • 低级错误:相对简单的错误,表明您的进程无法处理被交付的任何数据,但是,如果引起该错误的情况被简单地忽略,则进程将继续正常运行。这些更为人所知的是 bug。不要抛出或捕获它们,而是通过传递错误或其他有意义的失败指示符来防止它们发生,这些指示符可以由您的方法处理。例如:空参数异常。

  • 烦人错误:相对简单的错误,是您没有编写的其他代码抛出的。您必须捕获并处理所有这些错误,通常的方式与您自己的低级异常一样。请不要立即将它们再次抛出。例如:C# Int32.Parse() 方法中的格式异常。

  • 外部错误:这些错误非常直接,看起来很像(来自其他人的代码)或甚至Boneheaded(来自您的代码)情况,但必须抛出,因为现实规定抛出这些错误的代码真的不知道如何恢复,但可能会由调用方处理。请放心抛出,但当您的代码从其他地方收到它们时,请捕获并处理它们。例如:文件未找到异常。

四种异常中,外部异常是最需要考虑正确处理的。对于IO库方法而言,抛出文件未找到的异常是合适的,因为该方法几乎肯定不知道如果找不到文件该怎么办,特别是考虑到这种情况可能随时发生,且无法检测到该情况是否短暂。但是,对于应用程序级别的代码来说,抛出这样的异常就不合适了,因为该应用程序可以从用户处获取有关如何继续操作的信息。

文件未找到的情况可能不是最好的例子,因为程序在打开文件之前可能会检查文件是否存在。你是指程序确定文件存在后出现了文件未找到的异常(由于某种原因意外消失)吗? - Hibou57
"boneheaded" 异常不应该改为 assert 吗? - VF1
有人可能会误解“您必须捕获所有这些并处理它们”。确保以一种方式处理它们,以确保您的方法/代码仍然实现了预期的目标。最糟糕的事情是捕获异常并从未能够完成其预期工作的方法中静默返回。这被视为未处理的异常,因为您无法执行预期的目的。您需要将其上升到顶级日志记录/通知,以便发现错误,或者如果用户交互可以更正,则转换为用户友好的错误。 - AaronLS

6
  1. Never throw exceptions from destructors.

  2. Maintain some basic level of exception guarantees about the state of the object.

  3. Do not use exceptions to communicate errors which can be done using an error code unless it is a truly exception error and you might want the upper layers to know about it.

  4. Do not throw exceptions if you can help it. It slows everything down.

  5. Do not just catch(...) and do nothing. Catch exceptions you know about or specific exceptions. At the very least log what happened.

  6. When in the exception world use RAII because nothing is safe anymore.

  7. Shipping code should not have suppressed exceptions at least with regards to memory.

  8. When throwing exceptions package as much information as is possible along with it so that the upper layers have enough information to debug them.

  9. Know about flags that can cause libraries like STL to throw exceptions instead of exhibiting unknown behaviour (e.g. invalid iterators / vector subscript overflow).

  10. Catch references instead of copies of the exception object?

  11. Take special care about reference counted objects like COM and warp them in reference counted pointers when working with code that could throw exceptions.

  12. If a code throws an exception more than 2% of the time consider making it an error code for performance's sake.

  13. Consider not throwing exceptions from undecorated dll exports / C interfaces because some compilers optimize by assuming C code to not throw exceptions.

  14. If all that you do for handling exceptions is something akin to below, then don't use exception handling at all. You do not need it.

    main 
    {
         try {
        all code....
        }
        catch(...) {} 
    }
    

3

异常处理会消耗大量的处理时间,因此只有在应用程序中发生真正不应该发生的情况时才应该抛出异常。

有时您可以预测可能会发生什么样的事情并编写代码进行恢复,在这种情况下,适当的方法是抛出和捕获异常、记录并恢复,然后继续执行。否则,它们应该仅用于处理意外情况并优雅地退出,同时尽可能捕获更多信息以帮助调试。

我是一名.NET开发人员,对于catch和throw,我的方法是:

  1. 仅在公共方法中使用try/catch(通常情况下;显然,如果您正在捕获特定错误,则应在那里检查它)
  2. 仅在UI层中记录日志,然后在抑制错误并重定向到错误页面/表单之前。

异常与普通方法返回相比,为什么更耗费资源? - Daniel
好的,我再次选择 .NET,但是当抛出异常时会发生很多处理(展开调用堆栈,组合异常对象),这些在正常返回时不会发生。 - Guy Starbuck
2
除了在最深度的性能优化情况下,性能并不是选择处理错误方式的好方法。如果您已经在托管代码中,这不太可能成为一个阻碍问题。 - Dustman
当然,这不是一个“停机”问题,但您应该看一下.NET异常类的目的 - 它旨在打包并向上冒扩展错误信息,以处理意外或非典型情况。显然,它并不打算用作正常业务逻辑流程的一部分。 - Guy Starbuck

2
我认为最佳使用异常的方式取决于您使用的计算机语言。例如,Java比C++有更稳固的异常实现。
如果您正在使用C ++,我建议您至少尝试阅读Bjarne Stroustrup(C ++的发明者)在他的书《C ++编程语言》的附录E中对异常安全性的说明。
他花了34页的篇幅来解释如何以安全的方式处理异常。如果您理解了他的建议,那么这应该就是您需要知道的全部内容。

2
该回答所涉及的语言是Java。
对于可能出现的普通错误,我们直接处理它们(例如如果某些内容为空或null,则立即返回)。我们仅在异常情况下使用实际的异常(exception)。
但是,我们永远不会抛出已检查的异常(checked exceptions)。我们为自己特定的异常子类化(subclass RuntimeException),直接捕获它们,并处理其他库、JDK API等引发的异常。在代码内部,我们尝试捕获并记录异常(如果发生了确实不应该发生且你无法恢复的异常,例如批处理作业中找不到文件的异常),或者将异常包装在RuntimeException中,然后再抛出它。在代码外部,我们依靠异常处理程序最终捕获那个RuntimeException,无论是JVM或Web容器。
这样做的原因是避免在调用某个方法时到处创建强制的try/catch块,而你可能只有四个实例调用该方法,但只有一个可以实际处理异常。这似乎是规则而不是例外,因此如果第四个实例可以处理它,它仍然可以捕获它并检查异常的根本原因以获取实际发生的异常(而不必担心RuntimeException包装)。

我同意MetroidFan2002的观点。这让我想起了Gunjan Doshi在2003年发表的文章“异常处理的最佳实践”。链接为http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html。 - Yurii Soldak

2

我认为通常可以根据资源访问、数据完整性和数据有效性来确定异常的好方法。

访问异常

  • 创建或连接到任何类型的连接(远程,本地)。
    • 发生在:数据库,远程调用
    • 原因:不存在,已被使用或不可用,凭据不足/无效
  • 打开、读取或写入任何类型的资源
    • 发生在:文件I/O,数据库
    • 原因:锁定,不可用,凭据不足/无效

数据完整性

  • 可能有很多情况需要考虑数据的完整性
    • 它引用了什么,包含了什么...
    • 查找关于需要一组标准使数据保持清洁和有效格式的方法或代码的资源。
    • 例如:尝试将值为“bleh”的字符串解析为数字。

数据有效性

  • 这是提供的正确数据吗?(它的格式正确,但可能不是给定情况下的正确参数集)
    • 发生在:数据库查询,事务,Web服务
    • 例子:向数据库提交一行并违反约束

显然还有其他情况,但这些通常是我遵守需要的情况。


也许违反合同条款也可以加入到这个列表中。 - Hibou57

0

0

可能需要纠正/澄清一下,有一种被称为"契约驱动开发"(Contract-Driven Development)的策略,它在公共接口中明确记录每个方法的预期前置条件和保证后置条件。然后,当实现该方法时,任何阻止您满足合同中后置条件的错误都应该导致抛出异常。未满足前置条件被视为程序错误,应该导致程序停止。

我不确定契约驱动开发是否涉及捕获异常的问题,但一般来说,您只应该捕获您预期并可以合理恢复的异常。例如,大多数代码无法从内存不足异常中恢复,因此没有捕获的意义。另一方面,如果您尝试打开一个文件进行写入,您可以(并且应该)处理文件已被其他进程独占锁定或文件已被删除的情况(即使在尝试打开文件之前已检查了其存在性)。

正如另一位评论者指出的那样,您还应避免使用异常处理可以预期和避免的预期条件。例如,在.NET框架中,int.TryParse比使用try/catch的int.Parse更可取,特别是在循环或类似情况中使用时。


0

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