异常的处理机制
下面的代码展示了异常处理机制的标准用法
public static void SomeMethod() {
try {
// 此次编写可能发生异常的代码
}
catch (InvalidOperationException) {
// 从异常InvalidOperationException恢复的代码...
}
catch (IOException) {
// 从IOException恢复的代码...
}
catch (Exception) {
// C# 2.0之前, 这里的catch仅能捕捉到和CLS相容的异常
// 在C# 2.0版本中以及之后, 这里的catch能捕捉到和CLS兼容和非兼容的异常 因为不兼容CLS的异常会被包装成一个RuntimeWrappedException类型的对象抛出
throw; 重新抛出异常
}
catch {
// 所有C#版本中, 这里的catch能捕捉到和CLS兼容和不兼容的异常
throw; // 重新抛出异常
}
finally {
// 对try的任何操作进行清理
}
// 如果try没有抛出异常或者某个catch块捕捉到异常没有抛出或重新抛出,都会执行下finally下面的代码
}
try块包含可能会抛出异常的代码,catch块一般是对异常进行恢复和错误的记录,如对数据库进行批量操作时,如果发生异常,可以在catch块里面执行回滚的代码,finally块是对try的任何操作进行清理,只要异常被catch住了,finally块代码就一定会被执行。一个try块至少有一个关联的catch块或finally块。如果try块中代码没有任何异常,线程会跳过与之关联的所有的catch块,如果有finally就直接执行finally块代码,完毕后从finally后面代码继续执行。前面说过C#里面所有自定制的异常都必须派生自Exception类,所以这里catch的捕捉类型就必须是System.Exception或者是它的派生类型。因为一个try块可以对应多个catch块,而CLR是自上而下搜索一个匹配的catch块的,所以我们应该将派生程度最大也就是最具体的异常类型放在最前面的catch的捕捉类型里。而CLR一旦找到匹配的catch块,后面的catch块就不会搜索了。如果该try块相关联的catch中没有一个能够接受该异常,CLR将沿着调用堆栈向更高层搜索能够接受该异常的catch块,如果直到堆栈顶部依然没有找到能够处理该异常的catch块,就会发生一个未处理的异常,CLR就会终止进程。当CLR找到一个具有匹配捕捉异常类型的catch时,它会先执行从抛出异常的try块开始,到匹配异常的catch块为止的范围内所有的finally块,然后才执行这个catch块里面的代码,完后执行者catch对应finally的代码。理解这一句很重要,下面我们通过一段代码来说明这个执行的过程:
这段代码执行的结果如下:
namespace ExceptionDemo
{
class Program
{
static void Main(string[] args)
{
try
{
double result = Calculation.DividedBy(0, 0);
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("外层finally");
}
Console.ReadKey();
}
}
/// <summary>
/// 计算类
/// </summary>
public static class Calculation
{
/// <summary>
/// 除法
/// </summary>
public static double DividedBy(int a, int b)
{
double result = 0.0;
try
{
result = a / b;
}
catch (System.NotFiniteNumberException ex)
{
throw;
}
finally
{
Console.WriteLine("内层finally");
}
return result;
}
}
}
内层finally
尝试除以零。
外层finally
具体的执行过程是这样,当程序主函数Main执行Calculation.DividedBy(0, 0)方法时,在DividedBy方法内部执行语句result = a / b时会抛出一个 System.DivideByZeroException类型的异常,这个时候在DividedBy方法的没有找到和DivideByZeroException匹配的捕捉类型,CLR会去调用栈的更高一层搜索与异常匹配的捕捉类型,这样CLR就会搜索到Main函数里面的catch(Exception ex),找到这个具有匹配的捕捉类型的catch后,线程就会开始执行DividedBy方法内部的那个finally块,然后执行Main函数那个匹配的catch块内容,再然后执行Main函数里面那个finally块。
在catch的圆括号中我们除了可以指定异常的捕捉类型外,还可以指定一个异常变量,如ex,当该catch块匹配异常时,会把该异常对象的指引地址赋给这个变量。在catch块的末尾,我们有三种选择:
1)重新抛出相同的异常,用throw或者throw ex(ex为catch圆括号指定的变量名);
2)抛出一个不同的异常,这个异常可以是自定义的也可以是FCL中定义的;
3)让线程从catch块底部退出,继续执行后面的代码。
上面谈到throw和throw ex,这两种方式都是重新抛出相同异常,但有什么区别呢?
一个异常抛出的时候,CLR会记录throw指令的位置(抛出位置)。一个catch块捕捉到该异常时,CLR又会记录异常的捕捉位置,在catch块内访问抛出的异常对象的StackTrace属性可以查看异常抛出位置到异常捕捉位置之间的所有方法。它们的区别就是CLR对于异常抛出起始位置的认知,当你用throw ex的时候会让CLR认为这里是异常抛出位置的起点,而throw则不会改变CLR对异常抛出起始点的认知。拿上面DividedBy方法来说,如用throw ex则会让CLR认为这里是异常抛出的起点,如果用throw,CLR会认为result=a/b才是异常抛出的起点。不过比较奇怪的是,不管你是throw还是throw ex ,在调用栈高一层捕捉到这个异常时查看StackTrace的信息都是一样,记录的位置都是thow或throw ex的位置。
|