.NET Framework 中的异常处理

.NET Framework 中的异常(MSDN)


异常是正在执行的程序所遇到的任何错误情况或意外行为。 以下这些情况都可以引发异常:您的代码或调用的代码(如共享库)中有错误,操作系统资源不可用,公共语言运行时遇到意外情况(如无法验证代码),等等。 应用程序可以从其中一些情况恢复,但无法从其他情况恢复。 尽管可以从大多数应用程序异常中恢复,但不能从大多数运行时异常中恢复。

在 .NET Framework 中,异常是从 System.Exception 类继承的对象。 异常从发生问题的代码区域引发, 然后沿堆栈向上传递,直到应用程序处理它或程序终止。

异常与传统的错误处理方法


传统上,语言的错误处理模型依赖于语言检测错误和查找错误处理程序的独特方式,或者依赖于操作系统提供的错误处理机制。 运行时实现的异常处理具有以下特点:

  • 处理异常时不用考虑生成异常的语言或处理异常的语言。

  • 异常处理时不要求任何特定的语言语法,而是允许每种语言定义自己的语法。

  • 允许跨进程甚至跨计算机边界引发异常。

与其他错误通知方法(如返回代码)相比,异常具有若干优点。 不再有出现错误而不被人注意的情况。 无效值不会继续在系统中传播。 不必检查返回代码。 可以轻松添加异常处理代码,以增加程序的可靠性。 最后,运行时的异常处理比基于 Windows 的 C++ 错误处理更快。

由于执行线程例行地遍历托管代码块和非托管代码块,因此运行时可以在托管代码或非托管代码中引发或捕捉异常。 非托管代码可以同时包含 C++ 样式的 SEH 异常和基于 COM 的 HRESULT。

运行时如何管理异常


运行时使用基于异常对象和受保护代码块的异常处理模型。 出现异常时,创建一个 Exception 对象来表示该异常。

运行时为每个可执行文件创建一个异常信息表。 在异常信息表中,可执行文件的每个方法都有一个关联的异常处理信息数组(可以为空)。 数组中的每一项描述一个受保护的代码块、任何与该代码关联的异常筛选器和任何异常处理程序(catch 语句)。 此异常表非常有效,在没有发生异常时,它不会导致在处理器时间或内存使用方面出现性能损失。 仅在异常发生时使用资源。

异常信息表对于受保护的块有四种类型的异常处理程序:

  • finally 处理程序,每当块退出时它都会执行,而不论退出是由正常控制流引起的还是由未经处理的异常引起的。

  • 错误处理程序,它在异常发生时必须执行,但在正常控制流完成时不执行。

  • 类型筛选的处理程序,它处理指定类或该类的任何派生类的任何异常。

  • 用户筛选的处理程序,它运行用户指定的代码,来确定异常应由关联的处理程序处理还是应传递给下一个受保护的块。

每种语言根据自己的规范实现这些异常处理程序。 例如,Visual Basic 通过 catch 语句中的变量比较(使用 When 关键字)提供对用户筛选的处理程序的访问;C# 不实现用户筛选的处理程序。

异常发生时,运行时开始执行由下列两步组成的过程:

  1. 运行时在数组中搜索执行下列操作的第一个受保护块:

    • 保护包含当前执行的指令的区域。

    • 包含异常处理程序或包含处理异常的筛选器。

  2. 如果出现匹配项,则运行时会创建一个 Exception 对象来描述该异常。 然后运行时执行位于发生该异常的语句和处理该异常的语句之间的所有 finally 语句或错误语句。 请注意,异常处理程序的顺序很重要;最里面的异常处理程序最先计算。 还请注意,异常处理程序可以访问捕捉异常的例程的局部变量和本地内存,但引发异常时的任何中间值都会丢失。

    如果当前方法中没有出现匹配项,则运行时搜索当前方法的每一个调用方,并沿着堆栈一直向上查找。 如果任何调用方都没有匹配项,则运行时允许调试器访问该异常。 如果调试器不能附加到该异常,则运行时引发 AppDomain.UnhandledException 事件。 如果没有该事件的侦听器,则运行时转储堆栈跟踪并结束应用程序。

     

    备注:

    CLR如何捕获并处理异常

    http://www.cnblogs.com/bitfan/archive/2009/12/08/1616550.html

     

           对于任何一个.NET应用程序中的类,其所包容的方法都包容着一个异常处理表,如果此方法中没有使用try…catch…finally,则此表为空(即此方法生成的IL指令中不包容任何的异常处理子句)。

     

           .NET应用程序运行时,如果正在执行的某个方法引发了一个异常,CLR会首先将相应的异常对象推入计算堆栈,然后扫描此方法所包容的异常处理表查找处理程序,其处理过程可以简述如下:

     

           CLR获取引发异常的IL指令地址,然后从上到下地扫描异常处理表,取出每个catch子句中“.try”关键字后面跟着的用于定位“块”的起始和结束地址,判断一下引发异常的IL指令地址是否“落”入此地址范围中。如果是,取出“catch”关键字后跟着的异常类型,比对一下是否与抛出的异常对象类型一致(或相兼容),如果这个条件得到满足,CLR取出handler后的两个IL地址,“准备”执行这两个地址指定范围的IL指令(这就是catch指令块中的异常处理代码)。

     

           如果本方法所包容的异常处理表中找不到合适的catch子句,CLR会依据引发异常的线程所关联的方法调用堆栈,查找此方法的调用者所包容的异常处理表。

     

           此过程将一直进行下去,直到找到了一个可以处理此异常的处理程序为止。

     

           假设CLR在整个方法调用链的某个“环节”(即调用此方法的某个“祖先”方法)所包容的异常处理表中找到了可处理此异常的catch异常处理子句,它就作好了执行此子句所定义的异常处理指令代码块的“准备”。

     

           “扫描并查找相匹配的catch子句”过程,是CLR异常处理流程的第一轮。

     

           当找到了合适的异常处理代码后,CLR再“回到原地”,再次扫描引发异常方法所包容的异常处理表,这回,CLR关注的不再是catch子句,而是finally子句,如果找到了合适的finally子句(只需判断一下引发异常的IL指令地址是否“落入”某finally子句所监视的IL指令地址范围之内即可),CLR执行finally子句所指定的处理指令(即其handler部分所定范围内的IL指令)。

     

           “扫描并查找相匹配的finally子句”过程,是CLR处理异常流程的第二轮。

     

           这“第二轮”的扫描,开始于引发异常的方法,结束于最顶层的包容了那个引发异常的方法的方法(这句话很拗口,举个例子就清楚了,比如,如果你有一个嵌套了很深的函数调用语句,并且在被调用的最底层的函数中引发了异常,而你在顶层Main()函数中又用try...catch...finally包围了这一函数调用语句,则第2轮扫描会“直达”最顶层Main()方法的异常处理表,不会中途停止于找到了合适catch子句的那个中间“站”。

     

      在所有“下层”finally子句执行结束之后,相应的catch子句所指定的异常处理代码块才开始执行。之后,与此catch子句“同层”的finally子句所指定的异常处理代码块得到执行。

     

         但事情还没完,现在轮到所有包容被执行catch子句所在方法的“父辈”方法中的finally子句执行。

     

           经过两轮的扫描,CLR就完成了对.NET应用程序引发异常的捕获与处理工作。

     

           这里还遗留着一个问题:

     

           CLR找不到合适的catch异常处理子句怎么办?

     

           如果某.NET应用程序中根本没有定义处理某种异常类型的代码,而此程序在运行时又真的引发了这种类型的异常(真是哪壶不开提哪壶),那么CLR在第一轮扫描过程中,会一直“上溯”到Main()方法所包容的异常处理表,然后“无功而返”。

     

           紧接CLR会进行第二轮的扫描,执行所有“应该被执行”的finally子句。

     

           故事的尾声是:在执行完了所有finally代码后,CLR强制中止此进程所创建的所有线程(哪怕它们运行正常),由操作系统显示一个“出错”对话框,等用户响应后,或结束或附加一个调试器来调试这个进程。

posted on 2012-05-09 12:28  ※WYF※  阅读(1510)  评论(0编辑  收藏  举报