异常处理的好处:
1.利用异常处理,我们可以将资源清理代码放在一个固定的位置,并且确保它们得到执行。
2.利用异常处理,我们可以将处理异常的代码放在一个集中的位置。
3.利用异常处理,我们可以很容易定位和修复代码中的bug。
1.异常处理的演化
大多数Win32 API都返回FALSE来表示函数调用出现了问题,而调用者则可以通过调用GetLastError函数来获得这些问题的相关信息。另一方面,COM选择了返回一个HRESULT来描述问题。如果HRESULT的高位为1,则表示一个假设被违反,HRESULT的其余位表示的值则可以帮助我们判断问题的原因。
异常对象相较于32位的错误码值有这诸多的优势。每个异常对象都包含着一个描述字符串,利用该字符串,我们将能够知道到底是哪个参数导致了问题。该字符串可能包含一些额外的信息来帮助我们改善代码。另外,每个异常对象还包含着一个堆栈踪迹(stack trace),根据堆栈踪迹,我们可以知道是哪段代码路径导致了异常。
异常处理的另一个好处是我们不必在异常出现的地方捕获或者检测它们。我们不必再为每一个可能失败的语句或者方法调用都添加错误检测和矫正代码。
2.异常处理机制
void SomeMethod()
{
try
{
//我们在try快中放入那些需要恢复后再清理操作的代码
}
catch(InvalidCastException)
{
//我们在这个catch快中放入那些能够从InvalidCastException异常(或者任何继承自InvalidCastException的异常)中恢复的代码
}
catch(NullReferenceException)
{
//我们在这个catch快中放入那些能够从NullReferenceException异常(或者任何继承自NullReferenceException的异常)中恢复的代码
}
catch(Exception e)
{
//我们在这个catch快中放入那些能够从任何与CLS兼容的异常中恢复的代码
//当捕获到一个和CLS兼容的异常,我们通常应该将其重新抛出
throw;
}
catch
{
//我们在这个catch块中放入那些能够从任何与CLS兼容、或者不兼容的异常中恢复的代码
//当捕获到一个和CLS兼容的异常,我们通常应该将其重新抛出
throw;
}
finally{
//在finally快中我们放入那些对try块中所启动的操作进行清理的代码。不管是否有异常抛出,finally块中的代码总是会被执行
}
//如果try块美元抛出异常、或者一个catch块捕获了异常并且美元抛出别的异常或者重新抛出捕获到的异常,
//finally块后的代码会被执行
}
try块包含可能会抛出异常的代码。异常恢复代码应该放在一个或多个catch快中。我们应该为应用程序可以从中恢复的每一种异常事件创建以一个catch块。一个try块必须有至少以俄国与之相关联的catch块或者finally块,单独的try块美元任何意义
catch块中包含的是出现异常时需要执行的响应代码。
出现在catch关键字后的表达式被称作异常筛选器(exception filter)。异常赛选器本身是一个类型,它表示开发人员预料到的,并且可以从中恢复的一种异常情况。在C#中,一个捕获筛选器中的类型必须是System.Exception或者一个继承System.Execption的类型。
注意代码执行时时自上而下搜索catch块的,我们应该将更为具体的异常(即其基类中继承体系中离System.Object较远的那些类型)放在上面。实际上,如果更具体的catch块出现在离代码底部更近的位置,C#编译器将会产生一个错误,因为这样的catch块不可能被执行到。
如果与该try块相关联的捕获筛选器中没有一个能接受该异常,CLR将沿着调用堆栈向更高一层搜索能够接受该异常的捕获筛选器。如果在搜索到了调用堆栈的顶部还没有找到能够处理该异常的catch块,那么就会出现一个未处理异常。
在catch块的末尾,我们有三种选择:
(1)重新抛出所捕获的异常,向更高一层的调用堆栈中的代码通知该异常的发生。
(2)抛出一个不同的异常,向更高一层的调用堆栈中的代码提供更多的异常信息。
(3)让线程从catch块的底部退出
finally块中包含的代码是确保要被执行的代码。一般地,finally块中的代码执行的是一些资源清理操作,这些清理操作通常是对应的try块中的行为所需要的。
3.异常的本质
异常时对程序接口隐含假设的一种违反。我们定义的接口通常会有一些隐含的假设。当这些程序接口中的假设被违反时,异常就处理了。
一些特殊异常被抛出时发生的事情:
OutOfMemoryException 该异常在我们试图新建一个对象、而垃圾收集器又找不到任何可以用的内存时被抛出。
StackOverflowException CLR在线程耗尽所有的堆栈空间时会抛出该异常。
ExecutionEngineException CLR在检测到它的内部数据结构毁坏,或者自身的一下bug时会抛出该异常。
4.System.Exception类
System.Exception类型的属性
Message 只读 string 一段辅助性的消息文本,表示异常抛出的原因。
Sourec 读写 string 包含产生异常的程序集名称
StackTrace 只读 string 包含异常所经历的方法的名称和签名。该属性对于调试工作非常有用
TargetSite 只读 MethodBase 包含抛出异常的方法
HelpLink 只读 string 包含一个指向文档的URL,他可以帮助用户理解异常
InnerException 只读 Exception 如果当前的异常时在处理另一个异常时产生的,那么该属性表示前一个异常。该属性通常为null。Exception类型还提供有一个公有方法GetBaseException,它可以遍历所有内部异常组成的链表,并返回最开始抛出的那个异常。
HResult 读写 Int32 该属性为一个受保护属性,它仅用于托管代码和飞托管COM代码互曹作的情况
5.FCL定义的异常类
微软认为Exception应该是所有异常的基类型,另两个类型System.SystemException和System.ApplicationException都继承自Exception。
CLR自身抛出继承自SystemException的异常类型。大多数继承自SystemException的异常表示的情况不是很严重。
FCL类型定义的方法也应该抛出继承自SystemException的异常。
微软认为ApplicationException类型时一个专门为应用程序使用的保留的基类型。也就是说微软自己定义的异常不会继承自ApplicationException。(微软自己也没有这么做)
6.定义自己的异常类
通过抛出一个我们自己定义的异常类型实例,我们可以捕获代码精确地知道所发生的事情,并以合适的方式进行恢复。
注意,根据约定,一个异常类型的名称应该以“Exception”结尾。
作为一个一般的原则,异常层次结构应该宽而且浅:异常类型应该继承自一个与Exception向近类型,并且继承深度一般都不应该超过2到3层。如果我们定义的异常类型不打算作为其他类型的基类型,我们应该将其标示为sealed.
Exception基类型定义了三个公有构造器:
(1)一个无参(默认)的构造器,它创建一个异常类型的实例,并将所有的字段和属性设为默认值
(2)一个参数为String的构造器,它创建一个异常类型的实例,并将异常的消息文本设置为指定的字符串
(3)一个参数为String和Exception的构造器,它创建一个异常类型的实例,并将异常的消息文本和内部异常分别设为构造器参数中指定的String和Exception。
当定义自己的异常类型时,我们应该为其实现上述这三个构造器,并调用基类型中相应的构造器。
所有的异常类型都应该是可序列化的,因为只有这样异常对象才能在跨越应用程序域(AppDomain)或机器边界时到达封装处理、并在客户端重新抛出。要使新定义的异常类型成为可序列化类型,我们必须在类型哈桑应用一个[Serializeable]定制特性,并且如果类型定义了任何新的字段,我们还必须让她实现ISerialzable接口的GetOjbectData方法和一个特殊的受保护的构造器(两者的参数都为SerializationInfo何StreamingContext)。
7.如何正确使用异常
(1)避免过多的finally块
(2)避免捕获所有的异常
(3)从异常中顺利地恢复
(4)当异常无法修时,回滚部分完成的操作,为了正确的回滚部分完成的操作,我们的代码应该捕获所有的异常。
(5)隐藏实现细节
8.FCL中存在的一些问题
1.FCL的第一个问题时期中的很多代码都具有如下的构造:
catch{...}
catch(Exception){...}
catch(ApplicationException){...}
这些异常时不应该被一个类库所捕获并忽略的。
2.一下FCL代码会频繁地捕获一个异常,然后又抛出一二新的异常。
3.处理反射的方式。如果我们获得了一个方法的MethodInfo,并试图调用其Invoke方法,而真正被调用的方法又有可能会抛出异常。
4.System.Windows.Forms.DataGrid控件有一个公有属性CurrentCell。如果我们的代码在试图设置当前单元格的值的时候抛出了一个异常,DataGrid控件会捕获所有继承自Exception的异常,然后显示以俄国Windows消息框。
9.性能考虑
就作者个人的经验表明异常处理的好处远远超出了它所带来的任何性能的损失。
10.捕获筛选器
当一个异常被抛出时,CLR会向上遍历整个堆栈,查看每个catch块的捕获筛选器-既catcha关键字后指定的异常类型。
11.未处理异常
当一个异常被抛出时,CLR会搜索能够处理该异常的捕获筛选器。如果有捕获筛选器接受该异常对象,那么将出现一个未处理异常(unhandled exception)。未处理异常表明一种应用程序未曾预料到的情况。
类库开发人员不应该为未处理异常设置任何策略。应用程序开发人员应该对定义和实现这种策略拥有完全的控制。
一种解决未处理异常的策略:当客户端请求一个远程对象或者远程服务时,服务器端的一个线程将被唤醒以处理该请求。服务器代码应该被放在一个try块中执行,该try块要有一个关联的catch块来捕获所有异常。如果cathc块捕获到一个异常,那么该异常有关的信息将被作为服务器的相应发给客户端。当客户端代码检测到右异常从服务器返回时,他将对异常对象执行反序列化,然后将其抛出,这将使得客户端代码能够捕获服务器抛出的异常。
在考虑未处理异常时,我们应该清楚我们正在处理的为何种线程。
主线程 执行控制台应用程序(CUI)、或Windows窗体应用程序(GUI)的Main方法的那个线程为一个托管主线程
手工线程 应用程序代码使用System.Threading.Thread对象显示创建的线程成为手工线程
线程池线程 .NET框架中的许多特性多利用了内建于CLR中的线程池。
终止化线程 在垃圾收集器判定对象为不可达时,托管堆有一个线程专门执行对象的Finalize方法
非托管线程 一些线程创建时无需知道任何CLR的知识
12.异常堆栈踪迹
异常的踪迹指出了异常经过的路径中所发生的事件,他对于我们检测异常原因,进而修改错误代码来说非常有用。