我们在C#的try catch代码块中里面经常使用throw语句抛出捕捉到的异常,但是你知道吗使用throw ex和throw抛出捕获到的异常效果是不一样的。
异常捕捉的原理
首先先介绍一下C#异常捕捉的原理,默认情况下在C#的一个函数中(注意这里说的是在一个函数中,不是跨多个函数),只会将最后一个异常抛出的位置记录到异常堆栈中,也就是说在一个函数中无论你用throw语句抛出了多少次异常,异常堆栈中始终记录的是函数最后一次throw异常的位置,如下面代码的函数ThrowExceptionFunction中使用throw语句抛出了4次异常,但是在46行的代码处只显示函数ThrowExceptionFunction在32行抛出了异常,之前抛出的3次异常都没有被记录到异常堆栈之中。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ExceptionTest2 7 { 8 class Program 9 { 10 static void ThrowExceptionFunction() 11 { 12 try 13 { 14 try 15 { 16 try 17 { 18 throw new Exception("模拟异常"); 19 } 20 catch (Exception ex1) 21 { 22 throw; 23 } 24 } 25 catch (Exception ex2) 26 { 27 throw; 28 } 29 } 30 catch (Exception ex3) 31 { 32 throw; 33 } 34 35 } 36 37 38 static void Main(string[] args) 39 { 40 try 41 { 42 ThrowExceptionFunction(); 43 } 44 catch (Exception ex) 45 { 46 Console.WriteLine(ex.StackTrace);//因为C#会为每个函数的异常记录一次堆栈信息,而本例中有两个函数分别为ThrowExceptionFunction和Main,所以这里堆栈捕捉到了两个异常一个是在函数ThrowExceptionFunction中32行,另一个是Main函数中42行, 47 } 48 49 Console.ReadLine(); 50 } 51 } 52 }
在.net framework3.5及之前,函数中catch代码块抛出的异常无法准确捕捉到位置,如下面代码中Main函数最后一次抛出异常是在代码20行,但是在25行输出的信息中却显示异常是在代码29行抛出的,这应该是.net framework3.5及之前的一个BUG,在.net framework4.0中已经修复了这个问题。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ExceptionTest3 7 { 8 class Program 9 { 10 static void Main(string[] args) 11 { 12 try 13 { 14 try 15 { 16 throw new Exception("异常模拟"); 17 } 18 catch (Exception ex1) 19 { 20 throw; 21 } 22 } 23 catch (Exception ex2) 24 { 25 Console.WriteLine(ex2.StackTrace); 26 } 27 28 Console.ReadLine(); 29 } 30 } 31 }
上面我们说了C#只会将一个函数中最后一次抛出异常的位置记录到异常堆栈之中,那么有什么办法能将一个函数中抛出的所有异常都记录到异常堆栈中吗?答案是可以的,构造嵌套异常即可,如下代码所示:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ExceptionTest4 7 { 8 class Program 9 { 10 static void ThrowExceptionFunction() 11 { 12 try 13 { 14 try 15 { 16 try 17 { 18 throw new Exception("模拟异常1"); 19 } 20 catch (Exception ex1) 21 { 22 throw new Exception("模拟异常2", ex1); 23 } 24 } 25 catch (Exception ex2) 26 { 27 throw new Exception("模拟异常3", ex2); 28 } 29 } 30 catch (Exception ex3) 31 { 32 throw new Exception("模拟异常4", ex3); 33 } 34 35 } 36 37 38 static void Main(string[] args) 39 { 40 try 41 { 42 ThrowExceptionFunction(); 43 } 44 catch (Exception ex) 45 { 46 Console.WriteLine(ex.ToString());//要想输出函数ThrowExceptionFunction内抛出的所有异常,将ThrowExceptionFunction内部的异常都嵌套封装即可,然后在输出异常的时候使用ex.ToString()函数,就可以输出所有嵌套异常的堆栈信息 47 } 48 49 Console.ReadLine(); 50 } 51 } 52 }
上面代码中我们在函数ThrowExceptionFunction中将四个throw出来的异常都嵌套封装了,最后在Main函数中使用ex.ToString()函数即可输出完整的异常堆栈,在ThrowExceptionFunction函数中抛出的所有异常都显示在了ex.ToString()函数输出的堆栈列表之中。
throw ex和throw
我们知道在try catch的catch代码块捕捉到异常之后可以使用throw ex和throw将捕捉到的异常再抛出来,那么这两种写法有什么不同呢?
throw ex
throw ex这种写法会让C#重置异常的抛出点,我们来看这段代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace ExceptionTesting 8 { 9 class Program 10 { 11 static void InnerException() 12 { 13 throw new Exception("模拟异常"); 14 } 15 16 static void OuterException() 17 { 18 try 19 { 20 InnerException(); 21 } 22 catch (Exception ex) 23 { 24 throw ex; 25 } 26 } 27 28 static void Main(string[] args) 29 { 30 try 31 { 32 OuterException(); 33 } 34 catch (Exception ex) 35 { 36 Console.WriteLine(ex.StackTrace);//由于代码24行使用throw ex重置了异常抛出点,所以这里异常堆栈只能捕捉到代码32行和24行抛出的异常,但是13行的异常在堆栈中无法捕捉到 37 } 38 39 Console.ReadLine(); 40 } 41 } 42 }
可以看到使用throw ex会使得C#重置代码中异常的抛出点,从而让C#认为异常的原始抛出点应该是在代码24行,在异常堆栈中无法捕捉到代码13行所抛出的异常。
throw
使用throw和throw ex唯一的不同就是throw并不会让C#重置异常的抛出点,我们将上面代码中24行的throw ex改为throw如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace ExceptionTesting 8 { 9 class Program 10 { 11 static void InnerException() 12 { 13 throw new Exception("模拟异常"); 14 } 15 16 static void OuterException() 17 { 18 try 19 { 20 InnerException(); 21 } 22 catch(Exception ex) 23 { 24 throw; 25 } 26 } 27 28 static void Main(string[] args) 29 { 30 try 31 { 32 OuterException(); 33 } 34 catch(Exception ex) 35 { 36 Console.WriteLine(ex.StackTrace);//由于现在代码24行使用了throw抛出捕获到的异常,并没有重置原始异常的抛出点,所以这里异常堆栈不但能捕捉到代码32行和24行抛出的异常,还能捕捉到代码13行抛出的异常。 37 } 38 39 Console.ReadLine(); 40 } 41 } 42 }
由于这一次我们使用了throw来抛出代码24行中catch代码块中捕获到的异常,并没有重置异常的抛出点,因此在上面代码36行这一次异常堆栈输出了13行、24行、32行三个异常。