一,发生异常以后为了确保清理代码的执行是如此重要,以至于许多编程语言都提供了一些构造来简化清理代码的编写。例如只要使用了lock, using 和foreach 语句,C#编译器就会自动生成try/finally块。另外,重写一个类的析构器(Finalize方法)时,C#编译器也会生成try/finally块。使用这些构造时,编译器将你写的代码放到try块内部,并自动将清理代码放到finally块中。具体如下所示。
1)使用lock语句时,锁会在finally块中释放。
2)使用using语句时,会在finally块中调用对象的dispose方法。
3)使用foreach语句时,会在finally块中调用IEnumerator对象的Dipose方法。
4)定义析构方法时,会在finally块中调用基类的Finalize方法。
二,不要什么都捕捉
使用异常时,新手一个常识的错误是过于频繁或者不恰当地使用catch块。捕捉一个异常说明你预见到该异常,理解它为什么发生,并指导如何处理它。换句话说,是在为应用程序定义一个策略,用可靠性换取开发效率。
但是,大家经常看到下面这样的代码:
try{
//尝试执行程序员知道可能失败的代码.....
}
catch(Exception){
}
这段代码指出了他预见到了所有异常类型,并知道如何从所有状况中恢复。这很明显是不太可能的。如果一个类型是类库的一部分,那么在如何情况下,它都绝不能捕捉并“吞噬” 所有异常(装作异常没有发生),因为它不可能准确预知应用程序具体如何响应一个异常。除此之外,通过委托、虚方法或接口方法,类型会经常调用应用程序代码。如果应用程序代码抛出一个异常,应用程序的另一部分则可能预期要捕捉到这个异常。所以,你绝不能写一个“大小通吃”的类型,悄悄地“吞噬”这个异常,而是应该允许该异常在调用栈中向上移动,让应用程序代码针对性地处理这个异常。
如果异常未得到处理,CLR会终止进程。本系列稍后会讨论未处理的异常。大多数未处理异常都能在代码测试期间发现。为了修正这些未处理的异常,一个办法是修改代码来捕捉一个特定的异常,要么重写代码排除会造成异常的出错条件。在生产环境中运行的最终版本应该极少出现未处理的异常。而且应该相当健壮。
注意:某些时候,不能完成任务的一个方法可能检测到对象的一些状态已经损坏,而且这些状态无法修复。假如允许应用程序继续运行,可能造成不可预测的行为或者安全隐患。检测到这种情况时,方法不应抛出异常。相反,它应该调用System.Environment的FailFast方法,强迫进程立即终止。
三,得体地从异常中恢复
有的时候,在调用一个方法时,已经预料到它可能抛出某些异常。由于能预料到这些异常,所以可以写一些代码,允许应用程序从异常中得体地恢复并继续运行。下面是一个伪代码的例子:
public string CalculateSpreadsheetCell(Int32 row, Int32 column){
String result;
try{
result = /*计算电子表格单元格中的值*/
}
catch(DivideByZeroException){
result = "Can't show value: Divide by zero";
}
catch(OverflowException){
result ="Can't show value: Too big";
}
return result;
}
上述伪代码计算垫子表格的单元格的内容,将代表值的一个字符串返回给调用者,使调用者能在应用程序的窗口显示字符串。但是,单元格的内容可能是另外两个单元格相除的结果。如果作为分母的单元格包含0,CLR将抛出一个DivideByZeroException异常。在本例中,方法会捕捉这个具体的异常,返回并向用户显示一个特殊字符串。类似地,单元格的内容可能是另外两个单元格相乘的结果。如果结果超出该单元的取值范围,CLR将抛出一个OverflowException异常。同样地,会返回并向用户显示一个特殊字符串。
捕捉具体异常时,应充分掌握在什么情况下会抛出异常,并知道从捕捉的异常类型派生出了哪些类型。除非在catch块的末尾重新抛出捕捉到的异常,否则不要捕捉并处理System.Exception,因为不可能搞清楚在try块中全部可能抛出异常。例如,try块还可能抛出OutOfMemoryException和StackOverflowException, 而这只是所有可能的异常中非常普通的两个。