迄今爲止,CLR異常機制讓人關注最多的一點就是「效率」問題。其實,這裏存在認識上的誤區,由於正常控制流程下的代碼運行並不會出現問題,只有引起異常時纔會帶來效率問題。基於這一點,不少開發者已經達成共識:不該將異常機制用於正常控制流中。達成的另外一個共識是:CLR異常機制帶來的「效率」問題不足以「抵消」它帶來的巨大收益。
CLR異常機制至少有如下幾個優勢:程序員
另外,「異常」其名稱自己就說明了它的發生是一個小几率事件。因此,因異常帶來的效率問題會被限制在一個很小的範圍內。實際上,try catch所帶來的效率問題幾乎是能夠忽略的。在某些特定的場合,如Int32的Parse方法中,確實存在着由於濫用而致使的效率問題。在這種狀況下,咱們就應該考慮提供一個TryParse方法,從設計的角度讓用戶選擇讓程序運行得更快。另外一種規避由於異常而影響效率的方法是:Tester-doer模式數據庫
在異常機制出現以前,應用程序廣泛採用返回錯誤代碼的方式來通知調用者發生了異常。本建議首先闡述爲何要用拋出異常的方式來代替返回錯誤代碼的方式。對於一個成員方法而言,它要麼執行成功,要麼執行失敗。成員方法執行成功的狀況很容易理解,可是若是執行失敗了卻沒有那麼簡單,由於咱們須要將致使執行失敗的緣由通知調用者。拋出異常和返回錯誤代碼都是用來通知調用者的手段。網絡
可是當咱們想要告訴調用者更多細節的時候,就須要與調用者約定更多的錯誤代碼。因而咱們很快就會發現,錯誤代碼飛速膨脹,直到看起來彷佛沒法維護,由於咱們總在查找並確認錯誤代碼。
在沒有異常處理機制以前,咱們只能返回錯誤代碼。可是,如今有了另外一種選擇,即便用異常機制。若是使用異常機制,那麼最終的代碼看起來應該是下面這樣的:多線程
static void Main(string[]args) { try { SaveUser(user); } catch(IOException) { //IO異常,通知當前用戶 } catch(UnauthorizedAccessException) { //權限失敗,通知客戶端管理員 } catch(CommunicationException) { //網絡異常,通知發送E-mail給網絡管理員 } } private static void SaveUser(User user) { SaveToFile(user); SaveToDataBase(user); }
使用CLR異常機制後,咱們會發現代碼變得更清晰、更易於理解了。至於效率問題,還能夠從新審視「效率」的立足點:throw exception產生的那點效率損耗與等待網絡鏈接異常相比,簡直微不足道,而CLR異常機制帶來的好處倒是顯而易見的。分佈式
這裏須要稍增強調的是,在catch(CommunicationExcep-tion)這個代碼塊中,代碼所完成的功能是「通知發送」而不是「發送」自己,由於咱們要確保在catch和finally中所執行的代碼是能夠被執行的。換句話說,儘可能不要在catch和finally中再讓代碼「出錯」,那會讓異常堆棧信息變得複雜和難以理解。函數
在本例的catch代碼塊中,不要真的編寫發送郵件的代碼,由於發送郵件這個行爲可能會產生更多的異常,而「通知發送」這個行爲穩定性更高(即不「出錯」)。性能
以上經過實際的案例闡述了拋出異常相比於返回錯誤代碼的優越性,以及在某些狀況下錯誤代碼將無用武之地,如構造函數、操做符重載及屬性。語法特性決定了其不能具有任何返回值,因而異常機制被當作取代錯誤代碼的首要選擇。編碼
程序員,尤爲是類庫開發人員,要掌握的兩條首要原則是:
正常的業務流程不該使用異常來處理。
不要老是嘗試去捕獲異常或引起異常,而應該容許異常向調用堆棧往上傳播。
那麼,到底應該在怎樣的狀況下引起異常呢?線程
第一類狀況 若是運行代碼後會形成內存泄漏、資源不可用,或者應用程序狀態不可恢復,則應該引起異常。
在微軟提供的Console類中有不少相似這樣的代碼:設計
if((value<1)||(value>100)) { throw new ArgumentOutOfRangeException("value",value, Environment.GetResourceString("ArgumentOutOfRange_CursorSize")); }
或者:
if(value==null) { throw new ArgumentNullException("value"); }
在開頭首先提到的就是:對在可控範圍內的輸入和輸出不引起異常。沒錯,區別就在於「可控」這兩個字。所謂「可控」,可定義爲:發生異常後,系統資源仍可用,或資源狀態可恢復。
第二類狀況 在捕獲異常的時候,若是須要包裝一些更有用的信息,則引起異常。
這類異常的引起在UI層特別有用。系統引起的異常所帶的信息每每更傾向於技術性的描述;而在UI層,面對異常的極可能是最終用戶。若是須要將異常的信息呈現給最終用戶,更好的作法是先包裝異常,而後引起一個包含友好信息的新異常。
第三類狀況 若是底層異常在高層操做的上下文中沒有意義,則能夠考慮捕獲這些底層異常,並引起新的有意義的異常。
例如在下面的代碼中,若是拋出InvalidCastException,則沒有任何意義,甚至會形成誤解,因此更好的方式是拋出一個ArgumentException:
private void CaseSample(object o) { if(o==null) { throw new ArgumentNullException("o"); } } User user=null; try { user=(User)o; } catch(InvalidCastException) { throw new ArgumentException("輸入參數不是一個User","o"); } //do something}
須要重點介紹的正確引起異常的典型例子就是捕獲底層API錯誤代碼,並拋出。查看Console這個類,還會發現不少地方有相似的代碼:
int errorCode=Marshal.GetLastWin32Error(); if(errorCode==6) { throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ConsoleKeyAvailableOnFile")); }
Console爲咱們封裝了調用Windows API返回的錯誤代碼,而讓代碼引起了一個新的異常。
很顯然,當須要調用Windows API或第三方API提供的接口時,若是對方的異常報告機制使用的是錯誤代碼,最好從新引起該接口提供的錯誤,由於你須要讓本身的團隊更好地理解這些錯誤。
當捕獲了某個異常,將其包裝或從新引起異常的時候,若是其中包含了Inner Exception,則有助於程序員分析內部信息,方便代碼調試。
以一個分佈式系統爲例,在進行遠程通訊的時候,可能會發生的狀況有:
1)網卡被禁用或網線斷開,此時會拋出SocketException,消息爲:「因爲目標計算機積極拒絕,沒法鏈接。」
2)網絡正常,可是要鏈接的目標機沒有端口沒有處在偵聽狀態,此時,會拋出SocketException,消息爲:「因爲鏈接方在一段時間後沒有正確答覆或鏈接的主機沒有反應,鏈接嘗試失敗。」
3)鏈接超時,此時須要經過代碼實現關閉鏈接,並拋出一個SocketException,消息爲:「鏈接超過約定的時長。」
發生以上三種狀況中的任何一種狀況,在返回給最終用戶的時候,咱們都須要將異常信息包裝成爲「網絡鏈接失敗,請稍候再試」。
因此,一個分佈式系統的業務處理方法,看起來應該是這樣的:
try { SaveUser5(user); } catch(SocketException err) { throw new CommucationFailureException("網絡鏈接失敗,請稍後再試",err); }
可是,在提示這條消息的時候,咱們可能須要將原始異常信息記錄到日誌裏,以供開發者分析具體的緣由(由於若是這種狀況頻繁出現,這有多是一個Bug)。那麼,在記錄日誌的時候,就很是有必要記錄致使此異常出現的內部異常或是堆棧信息。
上文代碼中的:就是將異常從新包裝成爲一個CommucationFailureException,並將SocketException做爲Inner Exception(即err)向上傳遞。
此外還有一個能夠採用的技巧,若是不打算使用Inner Exception,可是仍然想要返回一些額外信息的話,可使用Exception的Data屬性。以下所示:
try { SaveUser5(user); } catch(SocketException err) { err.Data.Add("SocketInfo","網絡鏈接失敗,請稍後再試"); throw err; }
在上層進行捕獲的時候,能夠經過鍵值來獲得異常信息:
catch(SocketException err) { Console.WriteLine(err.Data["SocketInfo"].ToString()); }
你應該始終認爲finally內的代碼會在方法return以前執行,哪怕return是在try塊中。
C#編譯器會清理那些它認爲徹底沒有意義的C#代碼。
private static int TestIntReturnInTry() { int i; try { return i=1; } finally { i=2; Console.WriteLine("\t將int結果改成2,finally執行完畢"); } }
應該容許異常在調用堆棧中往上傳播,不要過多使用catch,而後再throw。過多使用catch會帶來兩個問題:
嵌套異常會致使 調用堆棧被重置了。最糟糕的狀況是:若是方法捕獲的是Exception。因此也就是說,若是這個方法中還存在另外的異常,在UI層將永遠不知道真正發生錯誤的地方。
除了第3點提到的須要包裝異常的狀況外,無端地嵌套異常是咱們要極力避免的。固然,若是真的須要捕獲這個異常來恢復一些狀態,而後從新拋出,代碼看起來應該是這樣的:
try{ MethodTry(); } catch(Exception) { //工做代碼 throw; }
或者:
try { MethodTry(); } catch { //工做代碼 throw; }
儘可能避免像下面這樣引起異常:
catch(Exception err) { //工做代碼 throw err; }
直接throw err而不是throw將會重置堆棧信息。
嵌套異常是很危險的行爲,一不當心就會將異常堆棧信息,也就是真正的Bug出處隱藏起來。但這還不是最嚴重的行爲,最嚴重的就是「吃掉」異常,即捕獲,而後不向上層throw拋出。若是你不知道如何處理某個異常,那麼千萬不要「吃掉」異常,若是你一不當心「吃掉」了一個本該往上傳遞的異常,那麼,這裏可能誕生一個Bug,並且,解決它會很費周折。
避免「吃掉」異常,並非說不該該「吃掉」異常,而是這裏面有個重要原則:該異常可被預見,而且一般狀況它不能算是一個Bug。 好比有些場景存在你能夠預見的但不重要的Exception,這個就不算一個bug。
若是須要在循環中引起異常,你須要特別注意,由於拋出異常是一個至關影響性能的過程。應該儘可能在循環當中對異常發生的一些條件進行判斷,而後根據條件進行處理。
處理未捕獲的異常是每一個應用程序應具有的基本功能,C#在AppDomain提供了UnhandledException事件來接收未捕獲到的異常的通知。常見的應用以下:
static void Main(string[]args) { AppDomain.CurrentDomain.UnhandledException+=new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Exception error=(Exception)e.ExceptionObject; Console.WriteLine("MyHandler caught:"+error.Message); }
未捕獲的異常一般就是運行時期的Bug,咱們能夠在App-Domain.CurrentDomain.UnhandledException的註冊事件方法CurrentDomain_UnhandledException中,將未捕獲異常的信息記錄在日誌中。值得注意的是,UnhandledException提供的機制並不能阻止應用程序終止,也就是說,執行CurrentDomain_UnhandledException方法後,應用程序就會被終止。
多線程的異常處理須要採用特殊的方法。如下的處理方式會存在問題:
try{ Thread t=new Thread((ThreadStart)delegate { throw new Exception("多線程異常"); }); t.Start(); } catch(Exception error) { MessageBox.Show(error.Message+Environment.NewLine+error.StackTrace); }
應用程序並不會在這裏捕獲線程t中的異常,而是會直接退出。從.NET 2.0開始,任何線程上未處理的異常,都會致使應用程序的退出(先會觸發AppDomain的UnhandledException)。上面代碼中的try-catch實際上捕獲的仍是當前線程的異常,而t屬於新起的異常,因此,正確的作法應該是把 try-catch放在線程裏面
Thread t=new Thread((ThreadStart)delegate { try { throw new Exception("多線程異常"); } catch(Exception error) { .... }); t.Start();
除非有充分的理由,不然通常不要建立自定義異常。若是要對某類程序出錯信息作特殊處理,那就自定義異常。須要自定義異常的理由以下:
1)方便調試。經過拋出一個自定義的異常類型實例,咱們可使捕獲代碼精確地知道所發生的事情,並以合適的方式進行恢復。
2)邏輯包裝。自定義異常可包裝多個其餘異常,而後拋出一個業務異常。
3)方便調用者編碼。在編寫本身的類庫或者業務層代碼的時候,自定義異常可讓調用方更方便處理業務異常邏輯。例如,保存數據失敗能夠分紅兩個異常「數據庫鏈接失敗」和「網絡異常」。
4)引入新異常類。這使程序員可以根據異常類在代碼中採起不一樣的操做。
這個不說了,自定義異常通常是從System.Exception派生。。事實上,如今若是你在Visual Studio中輸入Exception,而後使用快捷鍵Tab,VS會自動建立一個自定義異常類。
前面已經提到過,除非發生讓應用程序中斷的異常,不然finally老是會先於return執行。finally的這個語言特性決定了資源釋放的最佳位置就是在finally塊中;另外,資源釋放會隨着調用堆棧由下往上執行(即由內到外釋放)。
即避免在內部深到處理記錄異常。最適合記錄異常和報告的是應用程序的最上層,這一般是UI層。
並非全部的異常都要被記錄到日誌,一類狀況是異常發生的場景須要被記錄,還有一類就是未被捕獲的異常。未被捕獲的異常一般被視爲一個Bug,因此,對於它的記錄,應該被視爲系統的一個重要組成部分。
若是異常在調用棧較低的位置被記錄或報告,而且又被包裝後拋出;而後在調用棧較高位置也捕獲記錄異常。這就會讓記錄重複出現。在調用棧較低的狀況下,每每異常被捕獲了也不能被完整的處理。因此,綜合考慮,應用程序在設計初期,就應該爲開發成員約定在何處記錄和報告異常。
推薦看一下篇! 《多綫程/異步/並行/任務》