一、多个catch块
在最简单的情形下,一个try块有一个catch块。但是在现实中,你常常会遇到包含try块的语句能够触发许多可能发生的异常的情形。
1.接之前的例子修改,假定在用户传入一个无效参数(比如小于0的值)的情况下,修改Car的Accelerate()方法还会引发一个基础类库定义的异常ArgumentOutOfRangeException。
该异常类的构造函数接收的第一个字符串参数为错误参数的名称,然后是描述该错误的信息。
代码:
1 public void Accelerate(int delta) 2 { 3 if (delta < 0) 4 { 5 throw new ArgumentOutOfRangeException("delta", "Speed must be greater than zero!"); 6 } 7 ...... 8 }
catch逻辑现在可以为每种异常分别作出回应:
1 static void Main(string[] args) 2 { 3 Car myCar = new Car("Rusty", 90); 4 5 try 6 { 7 myCar.Accelerate(-10); 8 } 9 catch (CarIsDeadException e) 10 { 11 Console.WriteLine(e.Message); 12 } 13 catch (ArgumentOutOfRangeException e) 14 { 15 Console.WriteLine(e.Message); 16 } 17 Console.ReadKey(); 18 }
输出:
Speed must be greater than zero!
参数名: delta
2.当创建多个catch块的时候,必须注意一个异常引发后都将被第一个可用的catch处理。
为了阐明什么是第一个可用的catch块,看代码:
如上图所示,我们试图处理通过捕获普通System.Exception来处理包括CarIsDeadException和ArgumentOutRangeException在内的所有异常,这段异常处理逻辑产生了编译错误。问题出在(is-a关系)第一个catch块可以处理任何派生自System.Exception的异常,其中包括CarISDeadException和ArgumentOutOfRangeException,故而最终导致永远无法到达另外两个catch块!
catch块按照下面的原则结构化:
最前面的catch块捕获最特定的异常(最顶端的子类异常),最后面的catch捕获最普通的异常(异常基类)。往往在最后一个catch块中才处理System.Exception。
下面是正确的代码:
1 static void Main(string[] args) 2 { 3 Car myCar = new Car("Rusty", 90); 4 5 try 6 { 7 myCar.Accelerate(-10); 8 } 9 catch (CarIsDeadException e) 10 { 11 Console.WriteLine(e.Message); 12 } 13 catch (ArgumentOutOfRangeException e) 14 { 15 Console.WriteLine(e.Message); 16 } 17 catch (Exception e) 18 { 19 //处理其他的所有异常 20 Console.WriteLine(e.Message); 21 } 22 Console.ReadKey(); 23 }
二、通用的catch语句(不推荐使用)
C#也支持通用catch块,它不显式接收由指定成员引发的一场对象:
代码:
1 static void Main(string[] args) 2 { 3 Car myCar = new Car("Rusty", 90); 4 5 try 6 { 7 myCar.Accelerate(90); 8 } 9 catch 10 { 11 Console.WriteLine("Something bad happened..."); 12 } 13 Console.ReadKey(); 14 }
很明显,这不是处理异常的最佳途径,因为我们无法获取关于发生错误有意义的资料,因此不推荐使用。不过在C#中这样的构造确实是允许的,通过它可以用通用模式处理所有异常。
三、再次引发异常
可以在try块逻辑中向之前的调用者再次引发一个调用栈异常。要像这样,仅仅在catch块中使用throw关键字就行了,它通过调用逻辑链传递异常。
在catch块只能处理即将发生的部分错误时这样做很有用:
1 static void Main(string[] args) 2 { 3 Car myCar = new Car("Rusty", 90); 4 5 try 6 { 7 myCar.Accelerate(90); 8 } 9 catch (CarIsDeadException e) 10 { 11 Console.WriteLine("Something bad happened..."); 12 throw; 13 } 14 Console.ReadKey(); 15 }
运行结果:
先出现
再出现
说明:
1.CarIsDeadException传给了CLR,该异常由Main()方法再次引发。这样的话,最终用户将看到一个系统提供的错误对话框。通常情况下,只需要向调用者再次引发部分处理过的异常,使之能够恰当地处理将发生的异常。
2.这里没有显式重新跑出CarIsDeadException对象,而是使用了不带参数的throw关键字。这里并没有创建新的异常对象,只是重新抛出了原始的异常对象。这样做保护了原始目标的上下文。
三、内部异常
我们完全可以在处理其他异常的时候触发一个异常。例如,假设我们在一个特定的catch块中处理CarIsDeadException,在这个过程中视图将栈跟踪记录到C盘下名为carError.txt的文件中:
1 catch (CarIsDeadException e) 2 { 3 FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); 4 ... 5 }
如果指定的文件在C盘中不存在,调用File.Open()将导致一个FileNotFoundException!
如果在处理一个异常的时候遇到另外一个异常,最好的习惯是将这个新异常对象标识为与第一个异常类型相同得新对象中的”内部错误“。
我们之所以需要创建一个异常的新对象来等待处理,是因为声明一个内部错误的唯一途径就是将其作为一个构造函数参数。
原因:
在System.Exception基类中public Exception InnerException{ get; }是只读的无法赋值。但存在构造函数public Exception(string message, Exception innerException),因此声明一个内部错误的唯一途径就是就将其作为一个构造函数参数。
如下:
1 catch (CarIsDeadException e) 2 { 3 try 4 { 5 FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); 6 } 7 catch (Exception e2) 8 { //引发记录新异常的异常,还有第一个异常的相关信息 9 throw new CarIsDeadException(e.Message, e2); 10 } 11 }
说明:
1.在上例中,我们将FileNotFoundException对象作为CarIsDeadException的第二个参数传递进来。一旦确定了这个新对象,我们就向下一个调用者的栈引发异常,本例中下一个调用者是Main()方法。
2.既然Main()方法没有下一个调用者来捕获异常,我们需要通过错误对话框将其再次呈现出来。就像再次引发异常一样,记录内部异常通常仅仅在调用者能够在首次发生异常时将其更恰当地捕获处理的情况下才有用。
例如,在本例中如果调用者catch块逻辑能够利用InnerException属性来获取内部异常对象的详细信息的话,记录内部异常才有用。
四、finally块
一个try/catch块后面可能接着会定义一个finally块。finally块并不是必须有的,它是为了保证不管是否有异常,一组代码语句始终都能被执行。
使用:在更加现实的场景中,当读者准备进行销毁对象、关闭文件、断开数据库连接等操作时,将资源清理加入到finally快中确保操作正确执行。
结构化异常处理(五)处理多个异常,布布扣,bubuko.com