提问人:Daniel 提问时间:9/20/2008 最后编辑:LukeDaniel 更新时间:11/1/2011 访问量:8214
指导异常处理策略的原则是什么?[已结束]
What are the principles guiding your exception handling policy? [closed]
问:
处理例外涉及很多相对性。除了异常涵盖硬件和操作系统引发的错误的低级 API 之外,还有一个阴暗的区域,程序员可以决定什么构成异常,什么是正常情况。
如何决定何时使用例外?您是否有关于例外情况的一致政策?
答:
异常的处理时间成本很高,因此只有在应用中发生真正不应该发生的事情时才应引发异常。
有时,您可以预测可能发生什么样的事情,并编写代码从中恢复,在这种情况下,抛出并捕获异常、记录并恢复,然后继续是合适的。否则,它们应该仅用于处理意外情况并正常退出,同时捕获尽可能多的信息以帮助调试。
我是一名 .NET 开发人员,对于 catch and throw,我的方法是:
- 只在公共方法中尝试/捕获(一般来说;显然,如果你要捕获一个特定的错误,你会在那里检查它)
- 仅在禁止显示错误并重定向到错误页面/表单之前登录 UI 层。
评论
如果语言环境确实具有例外的概念,那么语言环境是否根据所使用语言的规范提出了例外?我想到了 Java 中的“除以零”,CONSTRAINT_ERROR或者 Ada 中的“除以零”,而 C 中什么都没有。
程序员在选择了一种在其构成中定义了异常的编程语言后,如何“决定”使用异常?
编辑:或者,您不是“使用”例外,而是说何时对“处理”异常制定有凝聚力和一致的政策?
编辑2:您可能想查看Steven Dewhurst的书“C++ Gotchas”中的免费章节,特别是Gotcha 64和Gotcha 65。虽然它侧重于 C++,但所涉及的课程在其他语言中很有用。
评论
其他人可能必须纠正/澄清这一点,但有一种策略叫做(我相信)“契约驱动的开发”,您可以在公共界面中明确记录每种方法的预期前提条件是什么,以及保证的后条件。然后,在实现该方法时,任何阻止您满足合同中的后置条件的错误都会导致抛出异常。不满足前提条件被视为程序错误,应导致程序中止。
我不确定合同驱动的开发是否涉及捕获异常的问题,但一般来说,您应该只捕获您期望的异常并可以从中合理恢复。例如,大多数代码无法有意义地从内存不足异常中恢复,因此没有必要捕获它。另一方面,如果您尝试打开文件进行写入,则可以(并且应该)处理该文件被另一个进程独占锁定的情况,或者该文件已被删除的情况(即使您在尝试打开它之前检查了它的存在)。
正如另一位评论者所指出的,您还应该避免使用异常来处理可以预期和避免的预期条件。例如,在 .NET Framework 中,int.TryParse 比 int 更可取。使用 try/catch 进行解析,尤其是在循环中使用时。
不应将异常用作在对象内部方法之间内部传递信息的方法,在本地应使用错误代码和防御性编程。
异常旨在将控制权从检测到错误的点传递到可以处理错误的位置(堆栈的上层),这可能是因为本地代码没有足够的上下文来纠正问题,而堆栈上层的内容将具有更多上下文,因此能够更好地组织恢复。
在考虑异常(至少在 C++ 中)时,您应该考虑 API 提供的异常保证。最低保证级别应为基本保证,但您应努力(在适当的情况下)提供强担保。如果您不使用来自关节 API 的外部依赖项,您甚至可以尝试提供不抛出保证。
注意不要将异常保证与异常规范混淆。
例外保证:
不保证:
在异常转义方法后,无法保证对象的状态 在这些情况下,不应再使用该对象。
基本保证:
在几乎所有情况下,这应该是方法提供的最低保证。 这保证了对象的状态被很好地定义,并且仍然可以一致地使用。
强力担保:(又名交易担保)
这保证了该方法将完全成功 否则将引发异常,并且对象状态不会更改。
无投掷保证:
该方法保证不允许任何异常传播到方法之外。 所有破坏者都应做出此保证。
|注意如果异常在传播时对析构函数进行转义
|申请将终止
评论
这篇来自 BEA(现在的 Oracle)的文章很好地阐述了如何去做:http://www.oracle.com/technology/pub/articles/dev2arch/2006/11/effective-exceptions.html。它有点像 Java,但您也应该能够将其用于其他环境。
这篇来自 Microsoft 高级软件设计工程师 Eric Lippert 的博客文章总结了一套优秀而简短的异常策略指南。
总之:
致命:可怕的错误,表明您的进程完全不可恢复。清理任何你能清理的资源,但不要抓住它们。如果你正在编写能够检测到这种情况的代码,请务必抛出。示例:内存不足异常。
骨头:相对简单的错误,表明您的流程无法对它所处理的任何数据进行操作,但如果简单地忽略导致错误的任何情况,则该错误将继续正常运行。这些更广为人知的是错误。不要抛出或抓住它们,而是要防止它们发生,通常是通过传递错误或其他有意义的失败指标,这些指标可以由您的方法处理。示例:Null 参数异常。
烦恼:你不拥有的代码给你带来的相对简单的错误。你必须抓住所有这些并处理它们,通常就像你处理自己的骨头例外一样。请不要再把它们扔回去。示例:C# 的 Int32.Parse() 方法的格式异常
外生性:相对简单的错误,看起来很像 Vexing(来自其他人的代码)甚至 Bonehead(来自你的代码)的情况,但必须抛出,因为现实表明抛出它们的代码真的不知道如何恢复,但调用者可能会。继续抛出它们,但是当您的代码从其他地方接收到它们时,请捕获它们并处理它们。示例:找不到文件异常。
在这四个中,外生的是最需要考虑的。指示未找到文件的异常适合为 IO 库方法抛出,因为如果找不到文件,该方法几乎肯定不知道该怎么做,特别是考虑到这种情况可能随时发生,并且无法检测情况是否是暂时性的。但是,抛出此类异常不适用于应用程序级代码,因为该应用程序可以从用户那里获取有关如何继续的信息。
评论
assert
给出这个答案的上下文是 Java 语言。
对于可能弹出的正常错误,我们会直接处理这些错误(例如,如果某些内容为 null、空等,则立即返回)。我们只在特殊情况下使用实际的例外。
但是,我们永远不会抛出选中的异常。我们将 RuntimeException 子类化为我们自己的特定异常,在适用的情况下直接捕获它们,至于其他库、JDK API 等抛出的异常,我们会在内部尝试/捕获并记录异常(如果发生了确实不应该发生的事情,并且您无法像批处理作业的文件未找到异常一样恢复)或者我们将异常包装在 RuntimeException 中,然后抛出它。在代码的外部,我们依靠异常处理程序来最终捕获该 RuntimeException,无论是 JVM 还是 Web 容器。
这样做的原因是,它避免了在可能有四个调用方法的实例但实际上只有一个实例可以处理异常的任何地方创建强制 try/catch 块。这似乎是规则,而不是(没有双关语的意思......ouch) 异常,因此,如果第四个可以处理它,它仍然可以捕获它并检查异常的根本原因以获取发生的实际异常(无需担心 RuntimeException 包装器)。
评论
作为 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 );
}
评论
永远不要从析构函数中引发异常。
维护有关对象状态的一些基本级别的异常保证。
不要使用异常来传达错误,这可以使用错误代码来完成,除非它是真正的异常错误,并且您可能希望上层知道它。
如果可以的话,不要抛出异常。它减慢了一切。
不要只是什么都不做。捕获您知道的异常或特定异常。至少记录发生了什么。
catch(...)
在例外情况下,请使用 RAII,因为没有什么是安全的了。
传送代码不应该禁止异常,至少在内存方面是这样。
抛出异常时,尽可能多地打包信息,以便上层有足够的信息来调试它们。
了解可能导致 STL 等库抛出异常而不是表现出未知行为(例如无效迭代器/向量下标溢出)的标志。
捕获引用而不是异常对象的副本?
在使用可能引发异常的代码时,请特别注意引用计数对象(如 COM),并在引用计数指针中扭曲它们。
如果代码在超过 2% 的时间内引发异常,出于性能考虑,请考虑将其设为错误代码。
考虑不要从不修饰的 dll 导出/C 接口抛出异常,因为某些编译器通过假设 C 代码不抛出异常来优化。
如果为处理异常所做的一切都类似于下面的内容,那么根本不使用异常处理。你不需要它。
main { try { all code.... } catch(...) {} }
我认为通常有一种很好的方法可以根据对资源的访问、数据的完整性和数据的有效性来确定异常。
访问例外
- 创建或连接到任何类型的连接(远程、本地)。
- 发生在:数据库、远程处理
- 原因:不存在、已在使用或不可用、凭据不足/无效
- 打开、读取或写入任何类型的资源
- 发生在:文件 I/O、数据库
- 原因:锁定、不可用、凭据不足/无效
数据完整性
- 在许多情况下,数据的完整性很重要
- 它引用了什么,它包含了什么......
- 查找有关方法或代码的资源,这些方法或代码需要一组条件才能使数据干净且格式有效。
- 示例:尝试将值为“bleh”的字符串解析为数字。
数据的有效性
- 这是提供的数据是否正确?(它的格式正确,但对于给定情况,它可能不是正确的参数集)
- 发生在:数据库查询、事务、Web 服务
- 示例:将行提交到数据库并违反约束
显然还有其他情况,但这些通常是我试图在必要时遵守的情况。
评论
我相信使用异常的最佳方式取决于您使用的计算机语言。例如,Java 的异常实现比 C++ 要可靠得多。
如果你使用的是C++,我建议你至少尝试阅读Bjarne Stroustrup(C++的发明者)对异常安全的看法。请参阅他的著作“C++编程语言”的附录E。
他花了 34 页的篇幅试图解释如何以安全的方式处理异常。如果你确实理解了他的建议,那么这应该就是你需要知道的全部。
我的异常处理策略可以在以下位置找到:
http://henko.net/imperfection/exception-handling-policy-throwing-exception/。
(希望推广网站不违反规则,但粘贴在这里的信息有点太多了。
评论