異常就是指成員沒有完成它的名稱所宣示的行動。 上面這段代碼會有異常,因為Troy去執行Love這個函數,然而其中girl根本就沒有賦值。本來Troy預期完成愛一個姑娘這個行動,結果發生了異常的事情,姑娘離開了Troy。 異常要解決的問題 很多行為(比如方法和屬性)很多時候都沒法返回錯誤代碼(比如vo ...
異常就是指成員沒有完成它的名稱所宣示的行動。
public class Girl { public string Name { get; set; } } public class Troy{ Girl girl; public void Love() { Console.WriteLine("Troy愛上了" + girl.Name); } }
上面這段代碼會有異常,因為Troy去執行Love這個函數,然而其中girl根本就沒有賦值。本來Troy預期完成愛一個姑娘這個行動,結果發生了異常的事情,姑娘離開了Troy。
異常要解決的問題
很多行為(比如方法和屬性)很多時候都沒法返回錯誤代碼(比如void方法,構造器,屬性的獲取設置),但他們仍然需要報告錯誤,於是異常就來解決這個問題。
也就是說異常處理機制實際上是為了返回可預知的錯誤代碼,而不是為了去捕獲未知的異常讓程式不報錯。(這一點非常重要)
不要去讓程式吞異常,不把異常暴露出來讓其繼續運行,反而可能使程式做出更錯誤的舉動。(有錯就改,別藏著)
那麼其實我在剛學習的時候一直有個疑問,我這個系統很多人在用啊,你如果不吞異常,那報黃頁不是更6?
現在我認為這並不矛盾,如果有異常就在catch後進行異常處理還原操作,然後寫日誌或者用一個統一的頁面去提示用戶出錯了,而不是把黃頁去給用戶看。(就像你告訴別人你得胃病了,用嘴和肢体語言表述都行,你剖開自己的肚子告訴別人你有病就是你的錯了啊)
.Net的異常處理機制
.Net的異常處理機制是基於windows提供的結構化異常處理機制(Structured Exception Handing,簡稱SEH)構建的。
異常處理的代碼就不演示了,說說三大塊
- try塊
- 一個try塊中如果能拋出同一個異常類的操作,卻要進行不同的異常恢復措施,那麼應該分成兩個try塊。
- try和finally到一起一般是執行資源清理操作(也可以用using哦)。
- catch塊
- 一個try塊可以關聯0個或多個catch塊。
- catch後面跟著圓括弧中的表達式稱為捕捉類型,異常捕捉類型必須是System.Exception或者它的派生類。
- CLR自上而下搜索異常,所以要將較具體的異常放在頂部。也就是說首先寫派生程度最大的異常,然後才是其基類,然後才是System.Exception或者不指定任何捕捉類型的catch塊。
- 如果拋出的異常沒有catch到,也就是說catch的類型沒有一個與拋出的異常匹配,那麼CLR就回去調用棧更高的一層搜索與異常匹配的捕捉類型。如果到了調用棧的頂部還是沒有匹配到catch塊,就會發生未處理的異常。而一旦找到匹配的catch塊,就會執行內層所有finally塊的代碼,否則內層所有finally塊的代碼都不會執行。也就是說下麵示例代碼中會報異常:
static void Main(string[] args) { try { FuncA(); } finally { Console.WriteLine("主函數Finally"); } Console.Read(); } static void FuncA() { try { Object obj = new DateTime(); int a = (int)obj;//這裡會報System.InvalidCastException異常 } catch(InvalidDataException)//表示不匹配,然後到調用棧的上一級也就是main函數,然而main函數中的try根本就沒有catch所以更談不上什麼匹配,也就是出現了一個未處理的異常 { //這裡完全不會執行 } finally {//雖然有Finally說好的,不論是否異常都會執行,然而此時上面的異常沒有catch到,
//所以已經異常報錯了,不會再執行到這裡。此時CLR會終止進程,相較於讓程式繼續運行造成不可預知的結果這樣更好 Console.WriteLine("函數A的Finally"); } } - catch塊的末尾有以下三種處理方法:
- 重新拋出相同的異常,向調用棧高一層的代碼通知該異常的發生,也就是throw;
- 拋出一個不同的異常,向調用棧高一層的代碼提供更豐富的異常信息,也就是throw ex;//這裡ex為新的異常對象
- 讓線程從catch塊底部退出,不向更高層拋異常。
- 代碼可向AppDomain的FirstChanceException事件登記,這樣只要AppDomain一發生異常就會收到通知,並且在CLR開始搜索任何catch塊之前就會調用這些事件回調函數。
- finally塊
- finally塊為保證會執行的代碼。
- 如果在catch內部和finally內部又拋出了異常,那麼在try中的異常不會被記錄,其信息將丟失。
System.Exception類
微軟規定所有CLS相容的編程語言都必須拋出和捕捉派生自該類型的異常。
一般來講也就這個類中也就三個屬性要註意:
- Message指出拋出異常的原因
- InnerException如果當前異常是在處理一個異常時拋出的,那麼InnerException中就是上一個異常。用公共方法GetBaseException可以遍歷內部異常鏈表,返回最初拋出的異常。
- StackTrace包含異常拋出前調用過的所有方法的名稱和簽名。它返回一個從異常拋出位置到異常捕捉未知的所有方法。
拋出異常
拋異常需要考慮兩個問題:
第一個是拋出什麼Exception類型的異常。應該選擇一個更有意義的類型。要考慮到調用棧中高處的代碼,要知道那些代碼如何判斷一個方法失敗從而執行得體的恢復代碼。作者強烈建議異常的繼承層次結構應該淺而寬,這樣就可以儘量少的創建基類。而基類意味著把眾多錯誤當做一個錯誤來處理。
第二個是向異常類型的構造器傳遞什麼字元串消息。
自定義異常類
看起來自定義異常類很簡單,只需要繼承System.Exception類就OK了,然而實際上這是個很繁瑣的事情。
因為從System.Exception類派生出來的所有類都應該是可序列化的,使它們能穿越AppDomain邊界去寫入日誌或者資料庫。而序列化就涉及到很多問題。
作者寫了個泛型異常類去簡化,我這裡就不寫了,實際上在格式上找個系統異常照著寫就行了:
別忘了在自定義類上面加上[Serializable]特性。
作者的玩法更高端一點,自己建個泛型異常類繼承Exception,然後將一些構造函數或者序列化函數寫在這個類中。個性化的異常信息作為泛型變數T傳給泛型異常類來使用,以此起到簡化作用。
設計規範和最佳實踐
- 不要什麼都捕捉
- 就像前面說的捕捉異常表明已經預見到了此異常,理解它為什麼發生,並知道如何處理它。如果catch了System.Exception就表明你確定預知到了一切異常,並且知道如何處理,仿若神明。
- 所以應該有針對性地捕捉異常,而不是吞噬異常,沒有捕捉到的異常請拋出。(有一種有趣的玩法就是用一個線程去吞噬異常然後給出結果,然後另一個線程去檢測結果然後重新拋出該異常)
- 發生不可恢復的異常時回滾部分完成的操作——維護狀態
- 捕捉到異常後看能否寫代碼簡單回滾,不行的話也可以用事務來處理。
- 隱藏實現細節來維繫協定
- 如果需要傳遞給上層更多的信息,可以直接在異常的Data屬性中添加信息
- 可以嘗試著用對用戶而言更形象的異常去包裝實際發生的異常然後拋出,但是必須將實際發生的異常作為這個更形象的異常的InnerException。
未處理的異常
未處理的異常就是指那些未catch到的異常(調用棧向上查找也沒catch到)。
應用程式應建立處理未處理異常的策略,而微軟建議開發人員接收CLR的預設策略。也就是說,應用程式發生未處理異常時,程式終止,windows會向事件日誌寫一條記錄。
可通過事件查看器查看該記錄:
還可用可靠性監視程式查看應用程式的更多細節
圖上顯示我的dota2在3月25號又崩了,點後面可以查看詳細信息。
我們可以將未處理的異常自己去寫日誌記錄下來,或者發郵件什麼的都行。而微軟的每種應用程式模型都有自己的與未處理異常打交道的方式。
然而對於服務端程式而言,發生了未處理的異常,理想情況下是記錄日誌,然後向客戶端發送通知,表明請求無法完成,最終終止伺服器應用程式。(這個太扯了,作者也說太理想了)
對於伺服器應用程式,與未處理異常有關的信息不應該返回客戶端,首先用戶這些信息用戶並不能解決,其次伺服器應該儘量少暴露自己的相關信息,防止被黑。(這個必須保證)
對異常進行調試
異常處理的性能問題
約束執行區域
代碼協定