不看此文,別說你懂異常處理
原創【51CTO.com原創稿件】在 .NET 中異常處理是一個龐大的模塊,專門用來處理程序中的已知可捕獲異常,這篇文章我將詳細講解異常處理的細節性的東西,其中包含了異常處理類型、自定義異常處理、多 catch 的異常處理以及異常處理的依賴。
一、異常處理類型
C# 允許我們編寫的代碼拋出從 System.Exception 派生的任何異常類型(這其中包括了間接派生和直接派生)。例如下面的代碼段:
- public class Demo
- {
- public int StringToNumber(string para)
- {
- string[] numberArray={"零","一","二","三"};
- int number = Array.IndexOf(numberArray,(para??throw new ArgumentNullException(nameof(para))));
- if (number <0)
- {
- throw new ArgumentException("參數值無法轉換為數字",nameof(para));
- }
- return number;
- }
- }
上述代碼使用了 throw 關鍵字拋出了異常,并且使用了特定的異常類型說明了發生異常的背景。在代碼中我們只用到了 C# 7.0 的新特性 throw 表達式 ,在 para 為 null 時會拋出 ArgumentNullException 異常,當 number 的值小于 0 的時候我們并沒有拋出 Exception 類型的異常,而是拋出了更能明確告知異常原因的 ArgumentException 類型的異常。我們從代碼中可以看到,當 para 參數為 null 時拋出的是 ArgumentNullException 類型的異常而不是 NullReferenceException 類型的異常。對于這兩個類型的異常好多開發人員其實并不清楚它倆的區別。其實它倆的區別還是很簡單的, ArgumentNullException 是在錯誤的傳遞了空值時拋出的,如果傳遞的是 非空的無效參數 則必須使用 ArgumentException 或者 ArgumentOutOfRangeException 。如果是底層運行時發現對象的值為空的時候才會拋出 NullReferenceException 類型的異常,這個異常一般來說開發人員不能隨意拋出,我們應該先判斷參數是否為空之后再使用參數,如果為空就拋出 ArgumentNullException 異常。
除了 NullReferenceException 異常外,還有五種派生自 System.SystemException 的異常不能自己拋出,只能有運行時拋出,它們分別是 System.StackOverflowException 、 System.OutOfMemoryException 、System.Runtime.InteropServices.COMException 、System.ExecutionEngineException 和 System.Runtime.InteropServices.SEHException 。同樣,開發人員盡量不在程序代碼中拋出 Exception 和 ApplicationException 異常,因為它們所反映出來的異常過于籠統,沒法為異常提供明確的信息。
在實際項目開發中有可能會遇到代碼執行到一定程度就會出現不安全或者無法恢復的狀態,這時代碼大多數情況下不會出現異常,因此我們在這種情況下就必須調用 System.Environemnt.FailFast 方法終止程序,這個方法會向實踐日志寫入一條消息之后馬上終止程序進程。 前面的代碼中我們還使用了 nameof 操作符,使用這個操作符首先是因為我們可以利用重構工具方便的自動更改標識符,另外如果參數名發生了變化我們能及時收到編譯錯誤。
針對這一節的內容我來做一個簡單的總結:
-
成員接收到錯誤的參數時應當拋出 ArgumentException 異常或者它的子類型異常;
-
在拋出 ArgumentException 異常或者子類型異常時必須設置 ParamName 屬性,也就是 nameof;
-
拋出的異常必須能明確表示異常的問題;
-
避免在意外獲得空值時拋出 NullReferenceException 異常;
-
不要拋出 System.SystemException 及其派生的異常;
-
不要拋出 Exception 和 ApplicationException 異常;
-
如果程序出現不安全因素時必須調用 System.Environemnt.FailFast 方法來終止程序的運行;
-
要向傳給參數異常類型的 ParamName 使用 nameof 操作符
Tip:參數異常類型包括 ArgumentNullException 、ArgumentNullException 、ArgumentOutOfRangeException
二、捕獲異常處理
捕獲異常處理這一節比較簡單,主要需要了解并掌握的是多 catch 塊和異常類型的順序問題以及 when 子句。
-
多 catch 塊 多個 catch 塊在 C# 中是比較常見的,我們前面一節說過拋出的異常必須能明確表示異常的問題,因此我們可以利用多 catch 塊解決一個代碼段中有可能出現的多種異常的情況,每個 catch 塊針對一種異常情況進行處理。我們來看一個簡單的代碼段:
- void OpenFile(string filePath)
- {
- try
- {
- //more code
- }
- catch(ArgumentNullException ex)
- {
- //more code
- }
- catch(DirectoryNotFoundException ex)
- {
- //more code
- }
- catch(FileNotFoundException ex)
- {
- //more code
- }
- catch(IOException ex)
- {
- //more code
- }
- catch(Exception ex)
- {
- //more code
- }
-
- }
上述代碼中我們一共定義了 5 個 catch 塊,當發生異常時會被對應的 catch 塊攔截并處理。這一小節就這么簡單,主要是多 catch 塊的使用,下一小節我將講解 catch 塊最重要的內容。
-
異常類型的順序 異常類型的順序是很多初學者甚至是部分多年的老程序員會犯的問題,我們從前面的代碼中也可以看到 Exception 異常位于最后的位置, IOException 位于倒數第二的位置,這是因為 Exception 異常是所有異常的父類,所有的異常都是直接或間接派生自它,而 IOException 又是 DirectoryNotFoundException 和 FileNotFoundException 的父類。根據異常匹配的順序,C# 會始終匹配第一個符合要求的異常,如果將父類異常放在子類異常的前面,那么再代碼出現異常的時候回直接匹配父類異常的 catch ,不再去匹配后面的子類異常 catch 。
Tip:不管在什么情況下都必須把 Exception 異常作為最后的 catch ,當程序中出現的異常沒有匹配任何 catch 塊時可以被 Exception catch 塊攔截并處理
-
when 子句 從 C# 6.0 開始, catch 塊支持條件表達式,這樣我們可以不根據異常類型來匹配程序中出現的異常。When 子句返回的時一個布爾值,當返回 true 時 catch 塊才會執行。我們來看一個使用 when 子句的例子:
- try
- {
- //more code
- }
- catch(Win32Exception ex) when (ex.NativeErrorCode==42)
- {
- //more code
- }
不過我們也可以在 catch 塊中使用 if 語句執行上面的條件檢查,但是這樣做的話整個 catch 塊的邏輯就變為先成為異常處理程序,再進行條件判斷,進而造成了在不滿足條件的情況下無法去執行別的符合要求的 catch 塊。如果使用了 when 子句程序就可以先檢查條件,在決定是否執行 catch 塊。但是 when 自己也有需要注意的地方,如果 when 子句中拋出了異常,那么這新的異常就會被忽略并且整個 when 子句返回值將變為 false 。
-
重新拋出異常 這里在簡單說一下異常的重新拋出,有些開發人員喜歡在 catch 塊中寫這段語句
throw ex
。這段語句存在一個致命的問題,在 catch 塊中這么寫將會拋出一個新的異常,那么將會造成所有的棧信息被更新進而丟失最初的棧信息造成難以定位問題。因此 C# 開發團隊設計出了可以不指定具體異常的方法,就是在 catch 塊中直接使用 throw 語句。這樣我們就可以判斷當前 catch 塊是否可以處理這個異常,如果不能就講原始棧信息拋出去。
三、常規 catch
C# 要求代碼拋出的任何對象都必須從 Exception 派生,從 C#2.0 開始,不管是不是從 Exception 派生的所有異常在進入程序集之后,都會被打包成從 Exception 派生的。結果是捕捉 Exception 的 catch 塊現在可捕捉前面的塊不能捕捉的所有異常。
-
簡述 C# 還支持常規 catch 塊,即 catch{} ,它的行為和 catch(Exception ex) 塊的行為一樣,唯一不同的是它不具備類型名和變量名。同樣它也必須位于所有 catch 塊的末尾。在代碼中如果同時存在常規 catch 塊和 catch(Exception ex) 塊編譯器就會顯示警告,因為程序會永遠匹配 catch(Exception ex) 塊而不去匹配常規 catch 塊。之所以 C# 中出現常規 catch 塊的原因是因為如果程序中存在調用的別的語言開發的程序集,并且該程序集在使用過程中拋出了異常,那么這個異常是不會被 catch(Exception ex) 塊所攔截,而是進入到未處理狀態,為了避免這個問題 c# 就推出了常規 catch 塊。
Tip:雖然常規 catch 塊具有強大的功能,但是它依然存在一個問題。它不具備一個可供訪問的異常實例,所以無法確定異常是無害的還是有害于程序的。
-
原理 常規 catch 所生成的 CIL 代碼是 catch(object),這就說明不管拋出什么類型它都可以捕獲得到。雖然生成的 CIL 代碼是 catch(object),但是我們不能在代碼中直接這么寫。常規 catch 塊無法捕獲不是派生自 Exception 的異常,因此 C# 在設計的時候將所有來自其他語言的異常都統一設置為 System.Runtime.InteropServices.SEHException 異常,因此常規 catch 塊既能捕獲繼承自 Exception 的異常,又能捕獲非托管代碼的異常。
四、規范
異常處理規范不是由微軟所規定的,而是開發人員在千千萬萬的項目中總結出來的,下面我們來看一下。
-
只捕獲可以處理的異常 通常我們只處理當前代碼可以處理的異常,而不能處理的異常將會拋出去,讓棧中層級高的調用者去處理。
-
不隱藏無法處理的異常 這個問題會發生在剛剛從事開發的人員身上,他們會捕獲所有異常即不處理也不拋出。這種情況下如果系統出現問題那么將逃過檢測。
-
少用 Exception 和常規 catch 塊 所有的異常都是繼承自 Exception ,因此使用 Exception 來處理異常并不是一個最優方法,而且某些異常需要馬上關閉程序進程。
-
避免在調用棧較低的位置報告或記錄異常 大部分調用棧較低的位置無法完整處理異常,所以只能拋出異常,并且如果在這些位置記錄異常并且再拋出異常會造成異常的重復記錄。
-
無法處理異常時,因使用 throw 而不是 throw ex 拋出一個新的異常會造成棧追蹤重置為重新拋出的位置,而不是重用原始拋出位置。因此如果不需要重新拋出不同的異常類型或者不是想故意隱藏原始調用棧,就應使用 throw ,允許相同的異常在調用棧中向上傳播。
-
避免在 catch 塊中重新拋出異常 如果在開發中發現捕獲的異常不能完整或恰當的處理,并且需要拋出異常那么我們就需要重新優化捕獲異常的條件。
-
避免在 when 子句中拋出異常 when 子句拋出異常會造成表達式的結果變為 false,進而不能運行 catch 塊。
-
避免以后 when 子句條件改變 這種情況常見于異常會因本地化而改變,那么這是我們將不得不改變 when 子句的條件。
五、自定義異常處理
一般來說拋出異常時我們應該使用 c# 為我們提供的異常類型。但是某些情況下我們還需自定義異常,例如我們編寫的 API 是由其他語言開發人員調用的,這時我們就不能拋出自己所使用的語言的異常,應該自定義異常讓調用者清晰明了的知道發什么么錯誤。
自定義異常一般都是從 Exception 或者其他異常類派生出來,這是唯一的要求。自定義異常還必須遵循如下三點要求:
-
異常名稱以 Exception 結尾;
-
必須包含無參構造函數、包含唯一一個參數類型為 string 的構造函數和同時獲取一個字符串以及一個內部異常作為參數的構造函數;
-
集成層次不能大于 5 層。
部分程序要求異常可以序列化,這時我們可以使用可序列化異常。我們只需要在自定義異常類型上加上 System.SerializableAttribute特性 或 實現ISerializable ,然后添加一個構造函數來獲取 SerializationInfo 和 StreamingContext 。這里需要注意的是如果你使用的是 .NET Core 2.0 以下版本那么將無法使用可序列化異常。
六、總結
作者簡介
朱鋼,筆名喵叔,國內某技術博客認證專家,.NET高級開發工程師,7年一線開發經驗,參與過電子政務系統和AI客服系統的開發,以及互聯網招聘網站的架構設計,目前就職于一家初創公司,從事企業級安全監控系統的開發。
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】