当CLR检测到某个正在运行的.NET应用程序处于一种特殊的正常执行顺序被打断的状态时,会生成一个异常对象来表示这个错误,并将此对象在方法调用堆栈中向上传送。如果一个程序引发了一个异常却没有处理,CLR将会中断此进程。
一、异常处理机制
1.基本语法
try { //可能引发异常的语句 } catch(Exception e) { //对异常进行处理的语句 } finally { //"无论如何都要执行的语句" }
2.finally语句块中也可能发生异常,如果这种情况发生,则先前的异常会被抛弃。如下示例
class Program { static void Main(string[] args) { try { Method1(); } catch (NotSupportedException ex) { Console.WriteLine("Main NotSupportedException"); } } private static void Method1() { try { Method2(); } catch (IndexOutOfRangeException ex) { Console.WriteLine("Method1 IndexOutOfRangeException"); } } private static void Method2() { int[] numbers = new int[10]; try { for (int i = 1; i <= 10; i++) { Console.WriteLine(numbers[i]); } } catch (IndexOutOfRangeException ex) { Console.WriteLine("Method2 IndexOutOfRangeException"); throw; } finally { throw new NotSupportedException("Method2 finally exception"); } } }
3.CLR的"两轮遍历"异常处理策略
当应用程序拥有多层嵌套的异常捕获结构时,如果最底层或中间层发生了异常,CLR将优先在引发异常的那一层搜索catch语句块,看看有没有"兼容"此类型异常的处理代码,如果没有,"跳到"上一层去搜索,如果上一层还没有,继续往上搜索,由此直到方法调用堆栈的最顶层。这就是CLR异常处理策略的第一轮遍历:查找合适的异常处理程序。
如果在某一层找到了异常处理程序,注意,CLR并不会马上执行之,而是回到"事故现场",再次进行第二轮遍历,执行所有"中间"层次的finally语句块,然后,执行找到的异常处理程序,最后,再从本层开始一直遍历到最顶层,执行所有的finally语句块。但是有一个问题,如果始终没有找到合适的异常处理程序会怎么样,你试试就知道了。
二、catch的"诱惑"
有的程序员害怕让用户看到异常的出现,于是把所有的异常隐藏起来。这种作法其实是应用了一种"鸵鸟"策略,据说当有危险降临而又无法挣脱时,鸵鸟就会将头插入沙中,欺骗自己安全了。现实应用中不建议采取这样的做法来处理异常。
try { //功能代码 } catch(Eexception ex) { //在此处"吃掉"所有异常,没有任何代码进行异常处理 }
三、CLR异常处理机制探秘
1.方法的异常处理表
static void Main(string[] args) { try { int number = Convert.ToInt32(Console.ReadLine()); } catch (FormatException ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("finally"); } }
使用ildasm工具反编译出来的代码框架如下所示:
从上述代码中可知:
C#编程语言中的单层的try.catch.finally结构会被转换为"两层嵌套"的类似结构,CLR通过执行leave指令在IL汇编程序的try、catch和finally指令块间跳转,实现所定义的异常捕获和处理逻辑。
单击ildasm的"view"菜单,取消"expand try/catch"选项,可以看到C#编译器生成的IL代码的真面目。
具体功能代码被统一地放置在方法IL代码的前半部分,而用于实现异常捕获的代码放在方法IL代码的后半部分,称为"异常处理表","ret"指令是两部分的分界线。C#编译器通过在合适的地方插入leave指令使得其在无异常的情况下,永远执行不到异常处理代码。异常处理表中的每一项代表一个异常处理子句,IL汇编程序使用try、catch、handler和finally关键字,配合相应地址对前面的功能代码自然分块。
2.CLR如何捕获并处理异常
CLR获取引发异常的IL指令地址,然后从上到下地扫描异常处理表,取出每个catch子句".try"关键字后面跟着的用于定位"块"的起始和结束地址,判断一下引发异常的IL指令地址是否落入到此地址范围中,如果中,取出".catch"关键字后跟着的异常类型,比对一下是否与抛出的异常类型一致或相兼容,如果这个条件得到满足,CLR取出".handler"后的两个IL地址,"准备"执行这两个地址范畴的IL指令。"扫描并查找相匹配的catch子句"过程,是CLR异常处理流程的第一轮。
当找到了合适的异常处理代码后,CLR再"回到原地",再次扫描引发异常方法所包容的异常处理表,这回CLR关注的不再是catch子句,而是finally子句,如果找到了合适的finally子句,CLR执行finally子句所指令的处理指令。"扫描并查找相匹配的finally子句"过程,是CLR处理异常流程的第二轮。