Exception Definition:
Exception And Error:
就像Mary Campione的“The Java Tutorial”中所写的:“exception就是在程序执行中所发生的中断了正常指令流的事件(An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions.)。”依照美国传统辞典(American Heritage
Dictionary)所解释的,error就是:“效果或情况背离了可接受的一般法则(The act or an instance of deviating from an accepted code of behavior.)”
Exceptions
1.可以是 可被控制(checked) 或 不可控制的(unchecked)
2.表示一个由程序员导致的错误
3.应该在应用程序级被处理
Errors
1.总是 不可控制的(unchecked)
2.经常用来用于表示系统错误或低层资源的错误
3.如何可能的话,应该在系统级被捕捉
How Exception Come From:
1.Exceptions due to programming errors
程序错误:这种异常时由于程序的错误而产生的(例如,NullPointerException
and IllegalArgumentException)。
2.Exceptions due to client code errors
代码调用错误:客户端代码试图违反API协议调用接口。如果异常能提供一些有用的信息,客户端可以采取一些替代的行为。例如:解析一个不符合格式要求的XML文档而抛出的异常。如果这个异常能包含一些有用的信息(例如xml文档的那个位置出错),那么客户端就可以利用这些信息来采取回复步骤了。
3.Exceptions due to resource failures
资源故障错误:例如系统内存不足,网络连接失败。客户端可以在一段时间后重试操作,或者记录资源失败,停止服务。
Exception Handling Practice:
1. Always clean up after yourself
如果你正在使用数据库连接或者网络连接时,一定要记住释放资源,及时你调用的接口只抛出unckecked exception, 也一定要在使用之后利用try - finally块来释放资源。
2. Never use exceptions for flow control 从来不要用异常来做流程控制。
生成/收集内存stack traces信息是一件代价高昂的事情,stack trace是用来调试的。在try-catch块时他会自动收集stacktrace信息,降低运行速度,而异常处理只有在异常情况的时候才使用。
3. Do not suppress or ignore exceptions 不要压制/忽略/吃掉异常
当一个checked exception从一个API被抛出时,它是在试图告诉你要采取一些必要措施来处理他,如果异常对你来说没有任何意义,那么就把他封装成unchecked exception并抛给上一层,千万不要用catch块{}来忽略/吃掉它,当作什么事都没发生过。
4. Do not catch top-level exceptions 不要捕捉顶层的异常
事实上,Unchecked exceptions 继承于 RuntimeException 类,而它又是继承Exception类,如果捕捉
Exception
类的话,同时也捕捉了RuntimeException,这样也同时忽略我们本来要抛给上层的Unchecked exceptions。5. Log exceptions just once 同一个异常只记录一次
同一个异常的stack trace被记录多次会给程序员检查原始异常源的日志stack trace带来不必要的麻烦/困惑,而且会带来更大的IO量,所以只记录一次。
6、Use XML remarks to document which exceptions a method can throw. 为异常添加文档,方便调用者使用。
7、 Do Not Catch Exceptions That You Cannot Handle.
1、不正确的日志写法可能导致严重的性能问题。开发人员不首先调用log.isxxEnabled ()来检查中某个log级别的log行为是否开放,而是直接调用logging方法。当你这样做时,日志代码总是在执行时返回异常的堆栈跟踪信息。但由于日志级别设置的太低,你可能永远看不到这些信息,你可能根本不知道什么事情发生了。首先检查日志记录级别,这应该被当作一个基本习惯,这会让你避免产生不必要的对象。
2、在Jason Clark的一篇介绍托管应用中的Exception文章中(MSDN开发精选2004年10月刊),概述了在应用程序中执行的未处理的异常处理程序的默认行为:
[1] 主线程上发生的未处理异常将导致该应用程序终止。
[2] 主线程以外的线程(包括手动线程,线程池线程和CLR的终结器线程)上发生的未处理异常将会被CLR处理。即发生异常时,你的应用程序没有任何表面迹象,仍会继续运行。
3、不要返回错误码。
4、要通过抛出异常的方式来报告操作失败。如果一个方法未能完成它应该完成的任务,那么应该认为这是方法层面的操作失败,并抛出异常。
5、考虑通过调用System.Environment.FailFast(New in .NET 2.0)来终止进程,而不要抛出异常,如果代码遇到了严重问题,已经无法继续安全地执行。
6、 不要在正常的控制流中使用异常,如果能够避免的话。
7、考虑抛出异常可能会对性能造成的影响。
8、要为所有的异常撰写文档,异常本质上是对程序接口隐含假设的一种违反。我们显然需要对这些假设作详细的文档,以减少用户代码引发异常的机会。
9、不要让公有成员根据某个选项来决定是否抛出异常。
例如:
// 不好的设计
public Type GetType(string path, bool throwOnError)
调用者要比方法设计者更难以决定是否抛出异常。
10、 不要把异常用作公有成员的返回值或输出参数。这样会丧失用异常来报告操作失败的诸多好处。
11、避免显式地从finally代码块中抛出异常。
12、 考虑优先使用System命名空间中已有的异常,而不是自己创建新的异常。
13、要使用自定义的异常类型,如果对错误的处理方式与其它已有异常类型有所不同。
14、 不要仅仅为了拥有自己的异常而创建并使用新的异常。
15、要使用最合理、最具针对性的异常。抛出System.Exception总是错的,如果这么做了,那么就想一想自己是否真地了解抛出异常的原因。
16、要在抛出异常时提供丰富而有意义的错误消息。要注意的是这些信息是提供给谁的,可能是其它开发人员,也可能是最终用户,所以这些信息应根据面向的对象设计。
17、 要确保异常消息的语法正确无误(指自然语言,如汉语、英语等)。
18、要确保异常消息中的每个句子都有句号。
这个看起来似乎过于追究细节了,那么想想这种情况:使用FCL预定义异常的Message信息时,我们有没有加过句号。如果我们提供的信息没有句号,其它开发人员使用时到底加不加句号呢?
19 避免在异常消息中使用问号和感叹号。
或许我们习惯于使用感叹号来”警示”某些操作有问题,扪心自问,我们使用的代码返回一个感叹号,自己会有什么感觉。
20、不要在没有得到许可的情况下在异常消息中泄漏安全信息。
21、考虑把组件抛出的异常信息本地化,如果希望组件为使用不用(自然)语言的开发人员使用。
22、不要“catch”一个Exception,却什么也不处理。如果您隐藏了异常,你永远不会知道是否发生过异常。如果用catch语句块捕获了某个特定类型的异常,并完全理解在catch块之后继续执行对应用程序意味着什么,那么我们说这种情况是对异常进行了处理。
如果捕获的异常具体类型不确定(通常都是如此),并在不完全理解操作失败的原因或没有对操作失败作出反应的情况下让应用程序继续执行,那么我们说这种情况是把异常吞了。
23、不要在框架(是指供开发人员使用的程序)的代码中,在捕获具体类型不确定的异常(如System.Exception、System.SystemException)时,把异常吞了。
24、避免在应用程序的代码中,在捕获具体类型不确定的异常(如System.Exception、System.SystemException)时,把错误吞了。
有时在应用程序中把异常吞了是可以接受的,但必须意识到其风险。发生异常通常会导致状态的不一致,如果贸然将异常吞掉,让程序继续执行下去,后果不堪设想。
25、不要在为了转移异常而编写的catch代码块中把任何特殊的异常排除在外。
26、考虑捕获特定类型的异常,如果理解异常产生的原因,并能对错误做适当的反应。
此时一定要能够确信,程序能够从异常中完全恢复。
27、不要捕获不应该捕获的异常。通常应允许异常沿调用栈向上传递。
这一点极为重要。如果捕获了不该捕获的异常,会让bug更难以发现。在开发、测试阶段应当把所有bug暴露出来。
28、要在进行清理工作时使用try-finally,避免使用try-catch。
对于精心编写的代码来说,try-finally的使用频率要比try-catch要高的多。这一点可能有违于直觉,因为有时可能会觉得:try不就是为了catch吗?要知道一方面我们要考虑程序状态的一致,另一方面我们还需要考虑资源的清理工作。
29、要在捕获并重新抛出异常时使用空的throw语句。这是保持调用栈的最好方法。
如果捕获异常后抛出新的异常,那么所报告的异常已不再是实际引发的异常,显然这会不利于程序的调试,因此应重新抛出原来的异常。
30、不要用无参数的catch块来处理不与CLS兼容的异常(不是继承自System.Exception的异常)。
有时候让底层代码抛出的异常传递到高层并没有什么意义,此时,可以考虑对底层的异常进行封装使之对高层的用户也有意义。还有一种情况,更重要的是要知道代码抛出了异常,而异常的类型则显得无关紧要,此时可以封装异常。
31、 考虑对较低层次抛出的异常进行适当的封装,如果较低层次的异常在较高层次的运行环境中没有什么意义。
32、 避免捕获并封装具体类型不确定的异常。
33、要在对异常进行封装时为其指定内部异常(inner exception)。这一点极为重要,对于代码的调试会很有帮助。
标准异常类型的使用
34、不要抛出Exception或SystemException类型的异常。
35、不要在框架(供其它开发人员使用)代码中捕获Exception或SystemException类型的异常,除非打算重新抛出。
36、 避免捕获Exception或SystemException类型的异常,除非是在顶层的异常处理器程序中。
37、 不要抛出ApplicationException类型的异常或者从它派生新类(参看4.2描述)。
38、 要抛出InvalidOperationException类型的异常,如果对象处于不正确的状态。
一个例子是向只读的FileStream写入数据。
√ 要抛出ArgumentException或其子类,如果传入的是无效参数。要注意尽量使用位于继承层次末尾的类型。
√ 要在抛出ArgumentException或其子类时设置ParamName属性。
该属性表明了哪个参数引发了异常。
public static FileAttributes GetAttributes(string path)
{
if (path == null)
{
throw new ArgumentNullException("path",
);
}
}
√ 要在属性的设置方法中,以value作为隐式值参数的名字。
public FileAttributes Attributes
{
set
{
if (value == null)
{
throw new ArgumentNullException("value",
);
}
}
}
× 不要让公用的API抛出这些异常。
抛出这些异常会暴露实现细节,而细节可能会随时间变化。
另外,不要显式地抛出StackOverflowException、OutOfMemeryException、ComException、SEHException异常,应该只有CLR才能抛出这些异常。
7、性能方面的考虑
我们在使用异常时常常会产生性能方面的顾虑,在调试的时候感觉尤其明显。这样的顾虑合情合理。当成员抛出异常时,对性能的影响将是指数级的。当遵循前面的规范,我们仍有可能获得良好的性能。本节推荐两种模式。
7.1 Tester-Doer 模式
有时候,我们可以把抛出异常的成员分解为两个成员,这样就能提高该成员的性能。下面看看ICollection<T>接口的Add方法。
ICollection<int> numbers = …
numbers.Add(1);
如果集合是只读的,那么Add方法会抛出异常。在Add方法经常会失败的场景中,这可能会引起性能问题。缓解问题的方法之一是在调用Add方法前,检查集合是否可写。
ICollection<int> numbers = …
…
if(!numbers.IsReadOnly)
{
numbers.Add(1);
}
用来对条件进行测试的成员成为tester,这里就是IsReadOnly属性;用来执行实际操作并可能抛出异常的成员成为doer,这里就是Add方法。
√ 考虑在方法中使用Test-Doer模式来避免因异常而引发的性能问题,如果该方法在普通的场景中都可能会抛出异常(引发异常的频率较高)。
前提是”test”操作要远比”do”操作快。另外要注意,在多线程访问一个对象时会有危险性。
7.2 Try-Parse 模式
与Tester-Doer 模式相比,Try-Parse 模式甚至更快,应在那些对性能要求极高的API中使用。该模式对成员的名字进行调整,使成员的语义包含一个预先定义号的测试。例如,DateTime定义了一个Parse方法,如果解析字符串失败,那么它会抛出异常,同时还提供了一个与之对应的TryParse方法,在解析失败时会返回false,成功时则通过一个输出参数来返回结果。
使用这个模式时注意,如果因为try操作之外的原因导致(方法)操作失败,仍应抛出异常。
√ 考虑在方法中使用Try-Parse模式来避免因异常而引发的性能问题,如果该方法在普通的场景中都可能会抛出异常。
.NET系统中的异常设计,主要涵盖了两个主要目的:一是异常类型表示,二是异常信息描述。两者区别:异常类型,主要是让程序自动识别问题设计的,也就是catch(异常类型);而信息描述是给人,用户/开发人员阅读的,就是new xxxException(异常消息)。也就是说在异常处理上,如果我是开发底层类库或基础服务,我关注的我该抛出什么类型的异常,此时的描述信息是和业务逻辑无关的(最好的例子就是.NET自带异常类的描述),就是说清楚出了什么问题而已,也不管怎么解决。如果我开发的应用层逻辑,我的抛出的异常是关注于信息表达的,这是要和人交互的层面。
类库和基础服务,主要是异常的生产者,本身就该没有或少有try/catch语句;而前端用户的业务逻辑,主要是异常的消费者,天然就该拥有一定数量的try/catch语句,来减少校验逻辑和开发负担,为用户给提示出和业务有关的错误信息,以及操作指导。
异常管理
Ø An exception management system should be well encapsulated and should abstract the details of logging and reporting from the application’s business logic.
Ø Exceptions represent a breach of an implicit assumption made within code.
Ø Exceptions are not necessarily errors.
Ø You should derive your custom application exceptions from ApplicationException.
异常检测
You should only catch exceptions when you need to specifically perform any of the
following actions:
Ø Gather information for logging
Ø Add any relevant information to the exception
Ø Execute cleanup code
Ø Attempt to recover
适当使用异常
Throwing exceptions is more expensive than simply returning a result to a caller. Therefore they should not be used to control the normal flow of execution through your code. In addition, excessive use of exceptions can create unreadable and unmanageable code.
异常传播
There are three main ways to propagate exceptions:
Ø Let the exception propagate automatically:With this approach, you do nothing and deliberately ignore the exception.
Ø Catch and rethrow the exception:With this approach, you catch and react to the exception, and clean up or perform any other required processing within the scope of the current method. If you cannot recover from the exception, you rethrow the same exception to your caller.
catch(TypeAException e) { // Code to do any processing needed. // Rethrow the exception throw; } |
Ø Catch, wrap, and throw the wrapped exception:If you cannot recover, wrap the exception in a new exception, and throw the new exception back to the caller. The InnerException property of the Exception class explicitly allows you to preserve a previously caught exception. This allows the original exception to be wrapped as an inner exception inside a new and more relevant outer exception.
catch(TypeBException e) { // Code to do any processing needed. // Wrap the current exception in a more relevant // outer exception and rethrow the new exception. throw(new TypeCException(strMessage, e)); } |
Note:As the exception propagates up the call stack, catch blocks can only catch the outer exception. Inner exceptions are programmatically accessible through the InnerException property, but they cannot be matched to a catch block.
什么时候使用内部异常
For example, consider a hypothetical method called LoadUserInfo. This method may load a user’s information from a file that is assumed to exist when the methodException Management in .NET 9tries to access it. If the file does not exist, a FileNotFoundException is thrown,which has meaning within the context of the LoadUserInfo method. However, as the exception propagates back up the call stack—for example, to a LogonUser method—a FileNotFoundException exception does not provide any valuable information. If you wrap the FileNotFoundException in a custom exception class (discussed later in this document)—for example, one called FailedToLoadUserInfoException—and throw the wrapper exception, it provides more information and is more relevant to the calling LogonUser method. You can then catch the FailedToLoadUserInfoException exception in the LogonUser method and react to that particular exception type rather than having to catch the FileNotFoundException, which is an implementation detail of another method.
自定义异常
This hierarchy allows your application to benefit from the following:
Ø Easier development because you can define properties and methods on your base application exception class that can be inherited by your other application exception classes.
Ø New exception classes created after your application has been deployed can derive from an existing exception type within the hierarchy. Existing exception handling code that catches one of the base classes of the new exception object will catch the new exception without any modifications to the existing code, interpreting it as one of the object’s base classes.
You should only create a new application exception class for an exception type that you need to react to and handle within your code that is not already available in your application exception hierarchy or in the Framework.
管理未处理异常
Ø You should configure exception management settings within your application’s Web.config file.
<customErrors defaultredirect="http://hostname/error.aspx" mode="on"> <error statuscode="500" redirect="/errorpages/servererror.aspx" /> <error statuscode="404" redirect="/errorpages/filenotfound.htm" /> </customErrors> |
Ø You should be aware that these settings only apply to ASP.NET files (that is, files with specific extensions, such as .aspx and .asmx).
Ø Page_Error:Page.Error += new System.EventHandler(Page_Error);
Ø Application_Error:
protected void Application_Error(Object sender, EventArgs e)
{
Exception exc = Server.GetLastError();
// Perform logging, send any notifications, etc.
}