我們在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行三個異常。
