异常是编写程序时经常碰到的一个类型,经验告诉我们,异常处理是一项细致的工作,代码量往往比正常逻辑代码更多更复杂。
异常是程序运行时发生的,比如连接远程服务获取数据超时等;异常与错误不同,前者是发生在运行时,函数尚未执行完毕退出,而后者则是函数已经执行完毕并正常退出,两者相同的是函数都没有获得预期的结果。
.NET的框架不像C语言需要使用特殊方法才能支持异常处理,而是在语言层面就通过try,catch和finally实现了异常处理。例如:
2 // Put code requiring graceful recovery and/or cleanup operations here...
3 }
4 catch (InvalidOperationException) {
5 // Put code that recovers from an InvalidOperationException here...
6 }
7 catch (IOException) {
8 // Put code that recovers from an IOException here...
9 }
10 catch {
11 // Put code that recovers from any kind of exception other than those above here...
12 // When catching any exception, you usually re-throw the exception.
13 // I explain re-throwing later in this chapter.
14 throw;
15 }
16 finally {
17 // Put code that cleans up any operations started within the try block here...
18 // The code in here ALWAYS executes, regardless of whether an exception is thrown.
19 }
.NET框架中异常处理机制采用类似事件冒泡的方法,如果函数发生了异常,首先被CLR截获,然后根据异常类型匹配catch代码块,如果匹配失败则由CLR调用Environment.FailFast结束当前进程,如果匹配成功则执行catch代码块,最后再执行finally语句块。
在C#中,所有的异常类型都继承自System.Exception,这就意味着catch代码必须按照类型的继承关系,从子类型到父类型按顺序编写,否则子类型的异常就会匹配执行到其父类型的catch语句。catch是用来发生异常后恢复状态或清理的,这部分代码要尽量简单且不会引发异常,否则catch中的异常将同样被CLR获取,有可能成为未处理异常向外抛出,导致CLR调用Environment.FailFast结束当前进程。finally同理亦然。
System.Exception包括了如下属性
Message:用来描述异常信息
Source: 用来描述程序集名称
StackTrace:用来显示运行时调用树
TargetSite:用来显示发生异常的方法
InnerException: 用来显示内部的异常
C#中关于异常的另一个关键字是throw,用来主动向CLR抛出异常,与Exception的StackTrace和TargetSite有关系,如以下程序
2 {
3 try
4 {
5 throwExpReset();
6 }
7 catch (Exception ex)
8 {
9 Console.WriteLine(ex.StackTrace);
10 Console.WriteLine(ex.TargetSite);
11 }
12
13 try
14 {
15 throwExpWithoutReset();
16 }
17 catch (Exception ex)
18 {
19 Console.WriteLine(ex.StackTrace);
20 Console.WriteLine(ex.TargetSite);
21 }
22
23 Console.ReadLine();
24 }
25
26 private static void throwExpReset()
27 {
28 try
29 {
30 int i = 1;
31 int j = 0;
32 int k = someMethod(i, j);
33 }
34 catch (Exception ex)
35 {
36 throw ex;
37 }
38 }
39
40 private static void throwExpWithoutReset()
41 {
42 try
43 {
44 int i = 1;
45 int j = 0;
46 int k = someMethod(i, j);
47 }
48 catch (Exception ex)
49 {
50 throw;
51 }
52 }
53
54 private static int someMethod(int i, int j)
55 {
56 return i / j;
57 }
58
上面程序不同之处在于一个方法只调用throw另一个调用throw异常,在DEBUG环境下,throwExpWithoutReset能够输出发生异常的方法名someMethod,而throwExpReset则不能输出异常的方法,因为throw一个异常时会通知CLR更改异常开始点,进而影响了异常的StackTrace和TargetSite,而单个的throw则不会通知CLR更改异常开始点,不改变原始异常的各种信息。然而,当在RELEASE环境下,这两个方法却都不能输入产生异常的方法名,这是因为C#的JIT编译器在RELEASE环境下出于优化的目的会把一些方法inline编译,由于没有了方法名,也就无法从异常信息中获取。为了能够获取异常的“真实”信息,需要对someMethod增加属性强制编译器不inline编译此方法,如下所示
2 private static int someMethod(int i, int j)
3 {
4 return i / j;
5 }
对于系统中未处理的异常,往往需要产生异常的方法和调用树用来还原产生异常时的环境,从而处理此异常,所以发生异常时又不能处理时,应该使用throw,除非需要抛出一个新异常,如果发现异常信息的粒度还不够细,则应考虑给方法属性增加防止编译器inline。