指导异常处理策略的原则是什么?
处理异常涉及很多相对性。除了低级API(异常涵盖了硬件和操作系统引发的错误)外,还有一个阴暗的区域,程序员可以在其中确定什么构成异常以及什么是正常情况。
我们如何决定何时使用例外?我们是否有关于例外的一致政策?
解决方案
异常在处理时间上很昂贵,因此,仅应在应用中确实不应发生的异常时才抛出异常。
有时,我们可以预测可能发生的事情,并从中恢复代码,在这种情况下,抛出并捕获异常,记录并恢复然后继续是合适的。否则,它们应仅用于处理意外情况并正常退出,同时捕获尽可能多的信息以帮助调试。
我是.NET开发人员,为了赶走,我的方法是:
- 仅尝试/捕获公共方法(通常;通常,如果我们正在捕获特定错误,则应在此处进行检查)
- 仅在抑制错误并重定向到错误页面/表单之前,先登录UI层。
语言环境没有按照规范提出异常。使用的语言中是否确实有例外的概念?我正在考虑Java中的"被零除",或者Ada中的CONSTRAINT_ERROR,而C中则完全没有。
在选择在其构成内定义了异常的编程语言之后,程序员如何才能"决定"使用异常?
编辑:不是要"使用"异常,而是要在什么时候对"处理"异常有一个连贯一致的政策?
Edit2:我们可能想看看Steven Dewhurst的书" C ++ Gotchas"中的免费章节,特别是Gotcha 64和Gotcha65. 尽管它专注于C ++,但是所涉及的课程在其他语言中也很有用。
其他人可能不得不更正/弄清这一点,但是有一种策略(我相信)是"合同驱动的开发",我们可以在公共界面中显式地记录每种方法的预期准备工作以及保证的后置条件。然后,在实现该方法时,任何妨碍我们满足合同中的后置条件的错误都将导致抛出异常。不满足前提条件被视为程序错误,应导致程序中止。
我不确定合同驱动的开发是否涉及捕获异常的问题,但是总的来说,我们应该只捕获我们期望并可以从中合理恢复的异常。例如,大多数代码无法从内存不足异常中有意义地恢复,因此捕获它毫无意义。另一方面,如果我们尝试打开文件进行写入,则可以(并且应该)处理该文件被另一个进程独占锁定的情况,或者该文件已被删除的情况(即使我们选中了该文件)存在,然后再尝试将其打开)。
如另一位评论者所述,我们还应该避免使用异常来处理可以预期和避免的预期条件。例如,在.NET框架中,int.TryParse优于使用try / catch的int.Parse,尤其是在循环等中使用时。
异常不应用作在对象内部方法之间内部传递信息的方法,而应在本地使用错误代码和防御性编程。
异常的设计目的是将控制权从检测到错误的位置传递到可以处理错误的位置(堆栈较高的位置),这可能是因为本地代码没有足够的上下文来纠正问题以及堆栈较高的内容将具有更多的上下文,从而能够更好地组织恢复。
在考虑异常(至少在C ++中)时,应考虑API产生的异常保证。最低保证级别应为基本保证,尽管我们应努力(在适当情况下)提供强有力的保证。如果我们不使用任何来自关节API的外部依赖项,则我们甚至可以尝试提供不抛出保证。
N.B.不要将异常保证与异常规范混淆。
例外保证:
不保证:
There is no guarantee about the state of the object after an exception escapes a method In these situations the object should no longer be used.
基本保证:
In nearly all situations this should be the minimum guarantee a method provides. This guarantees the object's state is well defined and can still be consistently used.
强有力的保证:(又名交易保证)
This guarantees that the method will completely successfully Or an Exception will be thrown and the objects state will not change.
无投掷保证:
The method guarantees that no exceptions are allowed to propagate out of the method. All destructors should make this guarantee. | N.B. If an exception escapes a destructor while an exception is already propagating | the application will terminate
来自bea(现为oracle)的这篇文章很好地阐述了如何进行:http://www.oracle.com/technology/pub/articles/dev2arch/2006/11/effective-exceptions.html。它有点假设Java,但我们也应该能够在其他环境中使用它。
微软高级软件设计工程师埃里克·利珀特(Eric Lippert)的这篇博客文章总结了一套出色而简短的异常策略指南。
简而言之:
- 致命:严重的错误,表明过程是完全不可恢复的。清理我们可以使用的所有资源,但不要抓住它们。如果我们编写的代码一定能够检测到这种情况,请抛出。示例:内存不足异常。
- 骨头:相对简单的错误,表明进程无法对正在处理的任何数据进行操作,但是如果只是忽略了导致该错误的任何情况,它将继续正常进行。这些被称为bug。不要抛出或者捕获它们,而通常通过传递错误或者其他可以由方法处理的有意义的失败指示符来防止它们发生。示例:空参数异常。
- Vexing:我们不拥有的相对简单的错误正在抛出。我们必须捕获所有这些并加以处理,通常的方式与处理我们自己的"骨头死"异常的方式相同。请不要再将它们扔掉。示例:通过C#的Int32.Parse()方法设置格式异常
- 外生的:相对简单的错误,看起来很像Vexing(来自其他人的代码),甚至是Boneheaded(来自代码)情况,但必须抛出,因为现实表明抛出它们的代码确实不知道如何恢复,但是来电者可能会。继续并扔掉它们,但是当代码从其他地方接收到它们时,请抓住它们并对其进行处理。示例:找不到文件异常。
在这四个中,外源性是我们必须考虑的最多的事情。指示未找到文件的异常适用于IO库方法,因为该方法几乎肯定不会知道如果找不到文件,该怎么办,特别是考虑到这种情况随时可能发生,并且存在这种情况。无法检测情况是否是暂时的。但是,引发此类异常不适用于应用程序级代码,因为该应用程序可以从用户那里获取有关如何继续的信息。
给出此答案的上下文是Java语言。
对于可能弹出的正常错误,我们将直接处理这些错误(例如,如果某些内容为null,为空等,则立即返回)。我们仅在特殊情况下使用实际例外。
但是,我们永远不会抛出检查异常。我们将RuntimeException子类化为我们自己的特定异常,并在适用的地方直接捕获它们,对于其他库,JDK API等抛出的异常,我们在内部进行try / catch并记录该异常(如果确实发生了某些事情,还没有,我们将无法像批处理作业中找不到文件的异常一样进行恢复),否则我们会将异常包装在RuntimeException中,然后将其抛出。在代码的外部,我们依靠异常处理程序最终捕获该RuntimeException,无论是JVM还是Web容器。
这样做的原因是,它避免了在可能有四个调用方法实例,但实际上只有一个实例可以处理该异常的地方创建强制try / catch块的方法。这似乎是规则,而不是(没有双关语意的……哎呀)异常,因此,如果第四个异常可以处理它,它仍然可以捕获并检查异常的根本原因,以获取实际发生的异常(无需担心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(...) {} }
我认为通常存在一种基于对资源的访问,数据的完整性和数据的有效性来确定异常的好方法。
访问异常
- 原因:不存在,已经使用或者不可用,凭证不足/无效
- 原因:锁定,不可用,凭据不足/无效
数据完整性
- 在方法或者代码中寻找资源,这些资源或者方法需要一套标准才能使数据干净并采用有效格式。
- 示例:尝试将值" bleh"的字符串解析为数字。
数据有效性
- 示例:将行提交到数据库并违反约束
显然还有其他情况,但通常是我尝试在需要时遵守的情况。
我相信,使用异常的最佳方法取决于我们所使用的计算机语言。例如,Java具有比C ++更可靠的异常实现。
如果我们使用的是C ++,我建议我们至少尝试阅读Bjarne Stroustrup(C ++的发明者)关于异常安全性必须说的内容。请参阅他的书" C ++编程语言"的附录E。
他花了34页试图解释如何安全地处理异常。如果我们确实了解他的建议,那应该就是我们所需要知道的全部。
我的异常处理政策可在以下位置找到:
http://henko.net/imperfection/exception-handling-policy-throwing-exception/。
(希望推广网站不是违反规则的,但是要在此处粘贴太多信息。)