27

处理异常涉及很多相对论。除了异常涵盖硬件和操作系统引发的错误的低级 API 之外,还有一个阴影区域,程序员可以在其中决定什么构成异常以及什么是正常情况。

您如何决定何时使用异常?您是否有关于例外情况的一致政策?

4

12 回答 12

30

异常不应用作在对象内部的方法之间在内部传递信息的方法,在本地您应该使用错误代码和防御性编程。

异常旨在将控制从检测到错误的点传递到可以处理错误的位置(堆栈上层),大概是因为本地代码没有足够的上下文来纠正问题和堆栈上层的东西将有更多的背景,从而能够更好地组织复苏。

在考虑异常时(至少在 C++ 中),您应该考虑 API 做出的异常保证。最低保证水平应为基本保证,但您应努力(在适当情况下)提供强有力的保证。如果您不使用来自特定 API 的外部依赖项,您甚至可以尝试提供不抛出保证。

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

例外保证:

无保证:

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

基本保证:

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

强有力的保证:(又名交易保证)

这保证了该方法将完全成功或抛出异常并且对象状态不会改变。

无投掷保证:

该方法保证不允许任何异常传播到该方法之外。所有的析构函数都应该做出这个保证。
| NB 如果在异常已经传播时异常从析构函数中转义
| 应用程序将终止

于 2008-09-20T01:50:05.720 回答
7

Microsoft 高级软件设计工程师 Eric Lippert 的这篇博客文章总结了一套出色而简短的异常策略指南

简而言之:

  • 致命:可怕的错误表明您的过程是完全不可恢复的。尽可能清理任何资源,但不要抓住它们。如果您正在编写能够检测这种情况的代码,请务必抛出。示例:内存不足异常。

  • Boneheaded:相对简单的错误表明您的进程无法对正在处理的任何数据进行操作,但如果导致错误的任何情况被简单地忽略,则会继续正常运行。这些更好地称为错误。不要抛出或捕获它们,而是防止它们发生,通常是通过传递错误或其他可以由您的方法处理的有意义的失败指标。示例:空参数异常。

  • 烦恼:您不拥有的代码会向您抛出相对简单的错误。您必须捕获所有这些并处理它们,通常与处理您自己的Boneheaded异常的方式相同。请不要再把它们扔回去。示例:来自 C# 的Int32.Parse()方法的格式化异常

  • 外生:相对简单的错误,看起来很像Vexing(来自其他人的代码)甚至Boneheaded(来自您的代码)的情况,但必须抛出,因为现实表明抛出它们的代码真的不知道如何恢复,但是来电者可能会。继续扔这些,但是当您的代码从其他地方收到它们时,抓住它们并处理它们。示例:找不到文件异常。

在这四个中,外源性是您必须考虑最多才能做到正确的那些。一个指示找不到文件的异常适合为 IO 库方法抛出,因为该方法几乎肯定不知道如果找不到文件该怎么办,特别是考虑到这种情况随时可能发生并且存在无法检测这种情况是否是暂时的。但是,抛出这样的异常不适用于应用程序级代码,因为该应用程序可以从用户那里获取有关如何继续的信息。

于 2008-09-20T03:38:46.677 回答
6
  1. 永远不要从析构函数中抛出异常。

  2. 维护关于对象状态的一些基本级别的异常保证。

  3. 不要使用异常来传达可以使用错误代码完成的错误,除非它是真正的异常错误并且您可能希望上层知道它。

  4. 如果可以提供帮助,请不要抛出异常。它减慢了一切。

  5. 不要只是catch(...)什么也不做。捕获您知道的异常或特定异常。至少记录下发生了什么。

  6. 在异常世界中使用 RAII 因为没有什么是安全的了。

  7. 至少在内存方面,运输代码不应该抑制异常。

  8. 当抛出异常时,尽可能多地打包信息,以便上层有足够的信息来调试它们。

  9. 了解可能导致 STL 等库抛出异常而不是表现出未知行为的标志(例如,无效的迭代器/向量下标溢出)。

  10. 捕获引用而不是异常对象的副本?

  11. 在使用可能引发异常的代码时,请特别注意 COM 之类的引用计数对象,并将它们扭曲到引用计数指针中。

  12. 如果代码在超过 2% 的时间内抛出异常,请考虑将其设为错误代码以提高性能。

  13. 考虑不要从未修饰的 dll 导出/C 接口中抛出异常,因为某些编译器通过假设 C 代码不抛出异常来进行优化。

  14. 如果您为处理异常所做的一切类似于下面的内容,那么根本不要使用异常处理。你不需要它。

    main 
    {
         try {
        all code....
        }
        catch(...) {} 
    }
    
于 2008-09-24T04:11:54.410 回答
3

异常在处理时间上是昂贵的,所以它们应该只在你的应用程序中确实不应该发生的事情发生时才被抛出。

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

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

  1. 仅在公共方法中尝试/捕获(一般而言;显然,如果您要捕获特定错误,您会在那里检查它)
  2. 仅在抑制错误并重定向到错误页面/表单之前登录 UI 层。
于 2008-09-20T00:52:16.873 回答
2

给出此答案的上下文是 Java 语言。

对于可能弹出的正常错误,我们直接处理(例如,如果某些内容为 null、空等,则立即返回)。我们只在特殊情况下使用实际异常。

但是,我们永远不会抛出已检查的异常。我们将 RuntimeException 子类化为我们自己的特定异常,在适用的地方直接捕获它们,对于其他库、JDK API 等抛出的异常,我们会在内部尝试/捕获并记录异常(如果确实发生了应该'没有,您无法像批处理作业的文件未找到异常一样恢复),或者我们将异常包装在 RuntimeException 中,然后抛出它。在代码的外部,我们依赖异常处理程序来最终捕获该 RuntimeException,无论是 JVM 还是 Web 容器。

这样做的原因是它避免了在可能有四个调用方法的实例但只有一个可以实际处理异常的地方创建强制的 try/catch 块。这似乎是规则,而不是(没有双关语......哎哟)异常,所以如果第四个可以处理它,它仍然可以捕获它并检查异常的根本原因以获得发生的实际异常(无需担心 RuntimeException 包装器)。

于 2008-09-20T03:58:47.737 回答
2

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

访问例外

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

数据完整性

  • 在很多情况下,数据的完整性很重要
    • 它引用了什么,它包含什么......
    • 查找有关需要一组标准以使数据干净且格式有效的方法或代码的资源。
    • 示例:尝试将值为 'bleh' 的字符串解析为数字。

数据的有效性

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

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

于 2008-09-24T04:38:33.270 回答
2

我相信使用异常的最佳方式取决于您使用的计算机语言。例如,Java 比 C++ 有更可靠的异常实现。

如果您使用 C++,我建议您至少尝试阅读 Bjarne Stroustrup(C++ 的发明者)关于异常安全的说法。请参阅他的书“C++ 编程语言”的附录 E。

他花了 34 页试图解释如何以安全的方式处理异常。如果您确实理解他的建议,那么这应该就是您需要知道的全部内容。

于 2009-07-01T06:52:50.143 回答
0

作为一名 C++ 开发人员,我自己的政策是不将我认为是公共 API 的异常抛出到我的类/模块(事实上,这是 COM 的要求)。但是,我在私有类实现中广泛使用异常。例如,使用 ATL:

HRESULT Foo()
{
    HRESULT hr = S_OK;
    try {
        // Avoid a whole lot of nested ifs and return code
        // checking - internal stuff just throws.
        DoStuff();
        DoMoreStuff(); // etc.
    } catch ( CAtlException& e ) {
        hr = e;
    }
    return hr;
}

void DoSomething()
{
    // If something goes wrong, AtlThrow( E_FAILED or E_WHATEVER ); 
}
于 2008-09-20T04:24:36.630 回答
0

其他人可能必须纠正/澄清这一点,但有一种策略称为(我相信)“合同驱动的开发”,您可以在公共界面中明确记录每种方法的预期前提条件以及保证的后置条件。然后,在实现该方法时,任何阻止您满足合同中的后置条件的错误都应该导致抛出异常。不满足先决条件被认为是程序错误,应导致程序中止。

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

正如另一位评论者所指出的,您还应该避免使用异常来处理可以预期和避免的预期条件。例如,在 .NET 框架中,int.TryParse 优于带有 try/catch 的 int.Parse,尤其是在循环等中使用时。

于 2008-09-20T01:34:48.707 回答
0

bea(现在是 oracle)的这篇文章很好地说明了如何去做:http ://www.oracle.com/technology/pub/articles/dev2arch/2006/11/effective-exceptions.html 。它有点假设 Java,但您也应该能够将它用于其他环境。

于 2008-09-20T02:26:24.870 回答
0

我的异常处理政策可以在以下位置找到:

http://henko.net/imperfection/exception-handling-policy-throwing-exception/

(希望宣传一个网站不会违反规则,但是在这里粘贴的信息有点太多了。)

于 2009-07-03T07:02:33.313 回答
-2

不是语言环境根据规范引发的异常。如果确实有例外的概念,那么使用的语言是什么?我正在考虑 Java 中的“除以零”,或者 Ada 中的 CONSTRAINT_ERROR 与 C 中的任何内容。

在选择了在其组成中定义了异常的编程语言之后,程序员如何“决定”使用异常?

编辑:或者不是“使用”异常,你的意思是什么时候有一个关于“处理”异常的连贯一致的政策?

Edit2:您可能想查看 Steven Dewhurst 的书“C++ Gotchas”中的免费章节,特别是 Gotcha 64 和 Gotcha 65。虽然它侧重于 C++,但所涉及的课程在其他语言中很有用。

于 2008-09-20T01:00:28.367 回答