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