“要……”描述的是總要遵循的規範(但特殊情況下,可能需要違反)。
“考慮……”描述的是一般情況下應該遵循的規範,但如果完全了解規範背後的道理,并有很好的理由不遵循它時,也不要畏懼打破正常。
“不要……”描述的是一些幾乎絕對不該違反的規範。
“避免……”則沒有那麼絕對,它描述的是那些通常并不好,但卻存在一些已知的可以違反的情況。
× 不要傳回錯誤碼。
前面第1節已經讨論了異常的種種好處,是以還是把異常作為報告錯誤的主要方法。記住每個異常都有兩種資訊:其一是異常資訊(Message屬性),其二是異常的類型,異常處理程式根據它來決定應該執行什麼操作。
√ 要通過抛出異常的方式來報告操作失敗。
如果一個方法未能完成它應該完成的任務,那麼應該認為這是方法層面的操作失敗,并抛出異常。
√ 考慮通過調用System.Environment.FailFast(New in .NET 2.0)來終止程序,而不要抛出異常,如果代碼遇到了嚴重問題,已經無法繼續安全地執行。
× 不要在正常的控制流中使用異常,如果能夠避免的話。
√ 考慮抛出異常可能會對性能造成的影響,詳見第7節。
√ 要為所有的異常撰寫文檔,異常本質上是對程式接口隐含假設的一種違反。我們顯然需要對這些假設作詳細的文檔,以減少使用者代碼引發異常的機會。
× 不要讓公有成員根據某個選項來決定是否抛出異常。
例如:
// 不好的設計
public Type GetType(string path, bool throwOnError)
調用者要比方法設計者更難以決定是否抛出異常。
× 不要把異常用作公有成員的傳回值或輸出參數。
這樣會喪失用異常來報告操作失敗的諸多好處。
× 避免顯式地從finally代碼塊中抛出異常。
√ 考慮優先使用System命名空間中已有的異常,而不是自己建立新的異常。
√ 要使用自定義的異常類型,如果對錯誤的處理方式與其它已有異常類型有所不同。
關于建立自定義異常類的的細節見第5節。
× 不要僅僅為了擁有自己的異常而建立并使用新的異常。
√ 要使用最合理、最具針對性的異常。
抛出System.Exception總是錯的,如果這麼做了,那麼就想一想自己是否真地了解抛出異常的原因。
√ 要在抛出異常時提供豐富而有意義的錯誤消息。
要注意的是這些資訊是提供給誰的,可能是其它開發人員,也可能是最終使用者,是以這些資訊應根據面向的對象設計。
√ 要確定異常消息的文法正确無誤(指自然語言,如漢語、英語等)。
√ 要確定異常消息中的每個句子都有句号。
這個看起來似乎過于追究細節了,那麼想想這種情況:使用FCL預定義異常的Message資訊時,我們有沒有加過句号。如果我們提供的資訊沒有句号,其它開發人員使用時到底加不加句号呢?
× 避免在異常消息中使用問号和感歎号。
或許我們習慣于使用感歎号來”警示”某些操作有問題,扪心自問,我們使用的代碼傳回一個感歎号,自己會有什麼感覺。
× 不要在沒有得到許可的情況下在異常消息中洩漏安全資訊。
√ 考慮把元件抛出的異常資訊本地化,如果希望元件為使用不用(自然)語言的開發人員使用。
6.2 處理異常
根據6.1節的讨論,我們可以決定何時抛出異常,然後為之選擇合适的類型,設計合理的資訊,下一步就是如何處理異常了。
如果用catch語句塊捕獲了某個特定類型的異常,并完全了解在catch塊之後繼續執行對應用程式意味着什麼,那麼我們說這種情況是對異常進行了處理。
如果捕獲的異常具體類型不确定(通常都是如此),并在不完全了解操作失敗的原因或沒有對操作失敗作出反應的情況下讓應用程式繼續執行,那麼我們說這種情況是把異常吞了。
× 不要在架構(是指供開發人員使用的程式)的代碼中,在捕獲具體類型不确定的異常(如System.Exception、System.SystemException)時,把異常吞了。
× 避免在應用程式的代碼中,在捕獲具體類型不确定的異常(如System.Exception、System.SystemException)時,把錯誤吞了。
有時在應用程式中把異常吞了是可以接受的,但必須意識到其風險。發生異常通常會導緻狀态的不一緻,如果貿然将異常吞掉,讓程式繼續執行下去,後果不堪設想。
× 不要在為了轉移異常而編寫的catch代碼塊中把任何特殊的異常排除在外。
√ 考慮捕獲特定類型的異常,如果了解異常産生的原因,并能對錯誤做适當的反應。
此時一定要能夠确信,程式能夠從異常中完全恢複。
× 不要捕獲不應該捕獲的異常。通常應允許異常沿調用棧向上傳遞。
這一點極為重要。如果捕獲了不該捕獲的異常,會讓bug更難以發現。在開發、測試階段應當把所有bug暴露出來。
√ 要在進行清理工作時使用try-finally,避免使用try-catch。
對于精心編寫的代碼來說,try-finally的使用頻率要比try-catch要高的多。這一點可能有違于直覺,因為有時可能會覺得:try不就是為了catch嗎?要知道一方面我們要考慮程式狀态的一緻,另一方面我們還需要考慮資源的清理工作。
√ 要在捕獲并重新抛出異常時使用空的throw語句。這是保持調用棧的最好方法。
如果捕獲異常後抛出新的異常,那麼所報告的異常已不再是實際引發的異常,顯然這會不利于程式的調試,是以應重新抛出原來的異常。
× 不要用無參數的catch塊來處理不與CLS相容的異常(不是繼承自System.Exception的異常)。
有時候讓底層代碼抛出的異常傳遞到高層并沒有什麼意義,此時,可以考慮對底層的異常進行封裝使之對高層的使用者也有意義。還有一種情況,更重要的是要知道代碼抛出了異常,而異常的類型則顯得無關緊要,此時可以封裝異常。
√ 考慮對較低層次抛出的異常進行适當的封裝,如果較低層次的異常在較高層次的運作環境中沒有什麼意義。
× 避免捕獲并封裝具體類型不确定的異常。
√ 要在對異常進行封裝時為其指定内部異常(inner exception)。
這一點極為重要,對于代碼的調試會很有幫助。
6.3 标準異常類型的使用
× 不要抛出Exception或SystemException類型的異常。
× 不要在架構(供其它開發人員使用)代碼中捕獲Exception或SystemException類型的異常,除非打算重新抛出。
× 避免捕獲Exception或SystemException類型的異常,除非是在頂層的異常處理器程式中。
× 不要抛出ApplicationException類型的異常或者從它派生新類(參看4.2描述)。
√ 要抛出InvalidOperationException類型的異常,如果對象處于不正确的狀态。
一個例子是向隻讀的FileStream寫入資料。
√ 要抛出ArgumentException或其子類,如果傳入的是無效參數。要注意盡量使用位于繼承層次末尾的類型。
√ 要在抛出ArgumentException或其子類時設定ParamName屬性。
該屬性表明了哪個參數引發了異常。
public static FileAttributes GetAttributes(string path)
{
if (path == null)
{

}
}
√ 要在屬性的設定方法中,以value作為隐式值參數的名字。
public FileAttributes Attributes
set
if (value == null)
{

}
× 不要讓公用的API抛出這些異常。
抛出這些異常會暴露實作細節,而細節可能會随時間變化。
另外,不要顯式地抛出StackOverflowException、OutOfMemeryException、ComException、SEHException異常,應該隻有CLR才能抛出這些異常。
7、性能方面的考慮
我們在使用異常時常常會産生性能方面的顧慮,在調試的時候感覺尤其明顯。這樣的顧慮合情合理。當成員抛出異常時,對性能的影響将是指數級的。當遵循前面的規範,我們仍有可能獲得良好的性能。本節推薦兩種模式。
7.1 Tester-Doer 模式
有時候,我們可以把抛出異常的成員分解為兩個成員,這樣就能提高該成員的性能。下面看看ICollection<T>接口的Add方法。
ICollection<int> numbers = …
numbers.Add(1);
如果集合是隻讀的,那麼Add方法會抛出異常。在Add方法經常會失敗的場景中,這可能會引起性能問題。緩解問題的方法之一是在調用Add方法前,檢查集合是否可寫。
…
if(!numbers.IsReadOnly)
{
}
用來對條件進行測試的成員成為tester,這裡就是IsReadOnly屬性;用來執行實際操作并可能抛出異常的成員成為doer,這裡就是Add方法。
√ 考慮在方法中使用Test-Doer模式來避免因異常而引發的性能問題,如果該方法在普通的場景中都可能會抛出異常(引發異常的頻率較高)。
前提是”test”操作要遠比”do”操作快。另外要注意,在多線程通路一個對象時會有危險性。
7.2 Try-Parse 模式
與Tester-Doer 模式相比,Try-Parse 模式甚至更快,應在那些對性能要求極高的API中使用。該模式對成員的名字進行調整,使成員的語義包含一個預先定義号的測試。例如,DateTime定義了一個Parse方法,如果解析字元串失敗,那麼它會抛出異常,同時還提供了一個與之對應的TryParse方法,在解析失敗時會傳回false,成功時則通過一個輸出參數來傳回結果。
使用這個模式時注意,如果因為try操作之外的原因導緻(方法)操作失敗,仍應抛出異常。
√ 考慮在方法中使用Try-Parse模式來避免因異常而引發的性能問題,如果該方法在普通的場景中都可能會抛出異常。