前言
自從.NET出現後,關于CLR異常機制的讨論就幾乎從未停止過。迄今為止,CLR異常機制讓人關注最多的一點就是“效率”問題。其實,這裡存在認識上的誤區,因為正常控制流程下的代碼運作并不會出現問題,隻有引發異常時才會帶來效率問題。基于這一點,很多開發者已經達成共識:不應将異常機制用于正常控制流中。達成的另一個共識是:CLR異常機制帶來的“效率”問題不足以“抵消”它帶來的巨大收益。CLR異常機制至少有一下幾個優點:
1、正常控制流會倍立即中止,無效值或狀态不會在系統中繼續傳播。
2、提供了統一處理錯誤的方法。
3、提供了在構造函數、操作符重載及屬性中報告異常的便利機制。
4、提供了異常堆棧,便于開發者定位異常發生的位置。
另外,“異常”其名稱本身就說明了它的發生是一個小機率事件。是以,因異常帶來的效率問題會倍限制在一個很小的範圍内。實際上,try catch所帶來的效率問題幾乎忽略的。在某些特定的場合,如Int32的Parse方法中, 确實存在這因為濫用而導緻的效率問題。在這種情況下,我們就應該考慮提供一個TryParse方法,從設計的角度讓使用者選擇讓程式運作得更快。另一種規避因為異常而影響效率的方法是:Tester-doer模式,下文将詳細闡述。
本章将給出一些在C#中處理CLR異常方面的通用建議,一幫助大家建構和開發一個運作良好和可靠的應用系統。
建議58、用抛出異常代替傳回錯誤代碼
建議59、不要在不恰當的場合下引發異常
建議60、重新引發異常時使用inner Exception
58、用抛出異常代替傳回錯誤代碼
在異常機制出現之前,應用程式普遍采用傳回錯誤代碼的方式來通知調用者發生了異常。本建議首先闡述為什麼要用抛出異常的方式來代替傳回錯誤代碼的方式。
對于一個成員方法來說,它要麼執行成功,要麼執行失敗。成員方法成功的情況很容易了解。但是如果執行失敗了卻沒有那麼簡單,因為我們需要将導緻執行失敗的原因通知調用者。抛出異常和傳回錯誤代碼都是用來通知調用者的手段。
假設我們要實作這樣一個簡單的功能:應用程式需要完成一次儲存建立使用者的操作。這是一個分布式的操作,儲存動作除了需要将使用者儲存在本地外,還需要通過WCF在遠端伺服器上儲存資料。負責儲存使用者的成員方法如下:
如果單純的看SaveUser方法,似乎一切都還不錯,在約定好了錯誤代碼後,調用者隻要接收到1或2,就知道到底是那裡出現了問題。但仔細研究會發現,如果方法執行失敗,似乎還可以挖掘出更多的原因。
假設在SaveToFile方法中,我們可能會遇到:
1、程式無資料存儲檔案寫權限導緻的失敗。
2、硬碟空間不足導緻的失敗。
在SaveToDataBase方法中,我們可能會遇到:
1、服務不存在導緻的失敗。
2、網絡連接配接不正常導緻的失敗。
當我們想要告訴調用者更多的細節的時候,就需要與調用者約定更多的錯誤代碼。于是我們很快就會發現,錯誤代碼飛速膨脹,直到看起來似乎無法維護。因為我們總在查找并确認錯誤代碼。
采用接下來的方法,可能會省略很大一部分的錯誤代碼:
這看上去不錯,即使存在更多的錯誤也可以将失敗資訊呈現給調用者或者上層使用者。然後僅僅呈現失敗資訊就可以了嗎?我們來看看這樣一種情況:給失敗通知增加稍微複雜一點的功能。
如果本地儲存失敗,要完成“通知運作本段代碼的客戶機管理者”的功能。通常情況下,僅僅隻需要顯示類似的資訊:“本地儲存失敗,請檢查使用者權限”。如果遠端儲存失敗,應用程式需要“發送一封郵件給遠端伺服器的系統管理者”。總金額個增加的功能導緻我們不能像處理“本地儲存失敗”那樣來處理“遠端儲存失敗”。
一切仿佛又回到了起點,在沒有異常處理機制之前,我們隻能傳回錯誤代碼,但是現在有了另一種選擇,即使用異常機制。如果使用異常機制,那麼最終的代碼看起來應該是下面這樣的:
使用CLR異常機制後,我們會發現代碼變得更清晰、更易于了解了。至于效率問題,還可以重新審視“效率”的立足點:throw exception産生的那點效率損耗與等待網絡連接配接異常相比,簡直微不足道,而CLR異常機制帶來的好處卻是顯而易見的。
這裡需要稍加強調的是,在catch(CommunicationException)這個代碼塊中,代碼所完成的功能是“通知發送”而不是“發送”本身,因為我們要確定在catch和finally中所執行的代碼是可以倍執行的。換句話說,盡量不要在catch和finally中再讓代碼“出錯”,那麼讓異常堆棧資訊變得複雜和難以了解。
在本例的catch代碼塊中,不要真得編寫發送郵件的代碼,因為發送郵件這個行為可能會産生更多的異常,而“通知發送”這個行為穩定性更高(即不“出錯”)。
以上通過實際的案例闡述了抛出異常相比于傳回錯誤代碼的優越性,以及在某些情況下錯誤代碼将無用武之地,如構造函數、操作符重載及屬性。文法特性決定了其不能具備任何傳回值,于是異常機制倍當作取代錯誤代碼的首要選擇。
59、不要在不恰當的場合下引發異常
最常見不易引發異常的情況是對在可控範圍内的輸入和輸出引發異常。如下面的代碼所示:
暫時可以發現此方法有兩處不妥:
1、判斷Age為負數。這是一個正常的業務邏輯,它不應該倍處理為一個異常。
2、應該采用Tester-Doer來驗證輸入。
我們現在來添加一個Tester方法
而調用的地方看起來是這樣的
程式員,尤其是類庫開發程式員,要掌握的兩條首要原則是:
正常的業務流程不應使用異常來處理。
不要總是嘗試去捕獲異常或引發異常,而應該允許異常向調用堆棧往上傳播。
那麼到底應該在什麼情況下引發異常呢?
第一種情況 如果運作代碼後會造成記憶體洩漏、資源不可用,或者應用程式狀态不可恢複,則引發異常。
第二種情況 在捕獲異常的時候,如果需要包裝一些更有用的資訊, 則引發異常。
這類異常的引發在UI層特别有用。系統引發的異常所帶的資訊往往更傾向于技術性的描述;而在UI層,面對異常的很可能是最終的使用者。如果需要将異常資訊呈現給使用者,更好的做法是先包裝異常,然後引發一個包含友好資訊的新異常。
第三種情況 如果底層異常在高層操作的上下文中沒有意義,則可以考慮捕獲這些底層異常,并引發新的有意義的異常。
例如下面的代碼中:
如果抛出InvalidCastException則沒有任何意義,甚至會造成誤解,是以更好的方式是抛出一個ArgumentException。
需要重點介紹的正确引發異常的典型例子就是捕獲底層API錯誤代碼,并抛出。檢視如下代碼:
很顯然當需要調用WIndows API或第三方API提供的接口時,如果對方的異常報告機制使用的是錯誤代碼,最好重新引發該接口提供的錯誤,因為你需要讓自己的團隊更好地了解這些錯誤。
建議60、重新引發異常時使用inner Exception
當捕獲了某個異常,将其包裝或重新引發異常的時候,如果其中包含了Inner Exception,則有助于程式員分析内部資訊,友善調試。
可以先來檢視以下代碼
相當于把Test方法中的異常當作Inner Exception,然後向上抛出。
意思其實也就是将異常進行簡單的封裝,然後繼續向上抛出,讓上層來捕獲異常資訊。
英語小貼士
1、I see. ——我明白了。
2、 I quit! ——我不幹了!
3. Let go! ——放手!
4. Me too. ——我也是。
5. My god! ——天哪!
6. No way! ——不行!
7. Come on. ——來吧(趕快)
8. Hold on. ——等一等。
9. I agree。 ——我同意。
10. Not bad. ——還不錯。
感謝您的閱讀,如果您對我的部落格所講述的内容有興趣,那不妨點個推薦吧,謝謝支援:-O。