異常是使用.NET時必然會遇到的問題,但是,有太多的開發人員沒有從API設計的角度考慮這個問題。在大部分工作中,他們自始至終都知道需要捕獲什麼異常以及哪些異常需要寫入全域日誌。如果你設計了可以讓你正確使用異常的API,則可以顯著減少修複缺陷的時間。
誰的錯?
異常設計背後的基本理論始於這樣一個問題,“誰的錯?”為了方便本文的討論,這個問題的答案將總是以下三者之一:
庫
應用程式
環境
當我們說“庫”有問題,我們是指當前執行的某個方法有內部缺陷。在這種情況下,“應用程式”是調用庫方法的代碼(這有點混雜難分,因為庫和應用程式代碼可能在相同的程式集中。)最後,“環境”是指應用程式之外一切無法控制的東西。
庫缺陷
最典型的庫缺陷是NullReferenceException。對庫而言,它沒有任何理由拋出可以被應用程式檢測到的Null 參考異常。如果遇到了空,則庫代碼應該總是拋出一個更具體的異常,說明什麼為空白以及如何糾正這個問題。對於參數而言,這顯然是一個ArgumentNullException異常。而如果屬性或欄位為空白,則InvalidOperationException通常更合適。
根據定義,任何錶明庫缺陷的異常都是該庫中需要修複的Bug。那並不是說應用程式代碼沒有Bug,而是說庫的Bug需要首先修複。只有那樣,才能讓應用程式開發人員知道他也犯了錯誤。
這樣做的原因是,可能有許多人使用同樣的庫。如果一個人在不應該傳入空的地方錯誤地傳入了空,則其他人想必也會犯同樣的錯誤。把NullReferenceException替換為一個可以清晰地顯示出什麼出錯的異常,應用程式開發人員立即就可以知道什麼出錯了。
“成功之核(The Pit of Success)”
如果你讀過有關.NET設計模式的早期文獻,那麼你會經常碰到短語“成功之核”。其基本思想是這樣的:讓代碼容易被正確使用,不容易被誤用,並確保異常可以告訴你哪裡出錯了。遵循這個API設計理念,幾乎可以保證開發人員一開始就編寫出正確的代碼。
這就是為什麼一個沒有注釋的NullReferenceException是如此糟糕。除了堆疊追蹤外(可能非常深入庫代碼),沒有任何資訊可以協助開發人員確定他們哪裡做錯了。另一方面,ArgumentNullException和InvalidOperationException則為庫作者提供了一種方法,讓他們可以嚮應用程式開發人員說明如何修複問題。
其他庫缺陷
下一個庫缺陷是ArithmeticException系列,包括DivideByZeroException、FiniteNumberException和OverflowException。再次,這總是意味著庫方法的內部缺陷,即使那個缺陷只是一個缺失的參數有效性檢查。
庫缺陷的另外一個例子是IndexOutOfRangeException。從語義上講,它和ArgumentOutOfRangeException沒什麼不同,參見IList.Item,但它只適用於數組索引器。由於應用程式代碼通常不會使用裸數組,所以這意味著,自訂的集合類會有Bug。
自.NET 2.0引入泛型列表以來,ArrayTypeMismatchException就很少見了。觸發該異常的情況相當怪異。根據文檔:
當系統無法將數組元素轉換成聲明的數群組類型時會拋出ArrayTypeMismatchException。例如,一個String類型的元素無法存入一個Int32數組,因為這兩種類型之間無法轉換。應用程式一般是不需要拋出這類異常的。
要做到這一點,前面提到的Int32數組必須存入一個Object[]類型的變數。如果你使用了原始數組,則庫需要對此進行檢查。由於這個原因及其他許多方面的考慮,最好是不要使用原始數組,而是將它們封裝到一個合適的集合類中。
通常,其他轉換問題是通過InvalidCastException異常反映出來的。回到我們的主題,類型檢查應該意味著永遠不會拋出InvalidCastException異常,而是向調用者拋出ArgumentException或InvalidOperationException異常。
MemberAccessException是一個基類,涵蓋了各種基於反射的錯誤。除了直接使用反射外,COM互操作和動態關鍵詞的不正確使用都會觸發該異常。
應用程式缺陷
典型的應用程式缺陷是ArgumentException及其子類ArgumentNullException和ArgumentOutOfRangeException。以下是其他你可能不知道的子類:
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
所有這些都明確地表明應用程式有錯誤,而問題就出在調用庫方法的行裡。那條語句的兩個部分都很重要。考慮下面的代碼:
foo.Customer = null;foo.Save();
如果上述代碼拋出了一個ArgumentNullException異常,那麼應用程式開發人員會很困惑。它應該拋出一個InvalidOperationException異常,說明當前行之前有什麼地方出了問題。
以異常為文檔
典型的程式員不閱讀文檔,至少不會首先閱讀文檔。相反,他或她會閱讀公用API,編寫一些代碼並運行。如果代碼不能正常運行,就到Stack Overflow上搜尋異常資訊。如果該程式員夠幸運,則很容易在那裡找到答案以及指向正確文檔的連結。但即使如此,程式員們很可能也不會真正地讀它。
那麼,作為庫作者,我們如何解決這個問題?第一步是直接將部分文檔複製到異常中。
更多個物件狀態異常
InvalidOperationException有一個眾所周知的子類ObjectDisposedException。它的用途顯而易見,然而,很少有可銷毀類會忘記拋出這個異常。如果忘記了,則常見的結果是拋出NullReferenceException異常。該異常是由Dispose方法將可銷毀子物件置為空白所導致的。
與InvalidOperationException密切相關的是NotSupportedException異常。這兩種異常很容易區分:InvalidOperationException是指“你現在不能那樣操作”,而NotSupportedException是指“你永遠不能對這個類做那種操作”。理論上講,NotSupportedException應該只在使用抽象介面時出現。
例如,一個不可變集合在遇到IList.Add方法時應該拋出NotSupportedException異常。相比之下,一個可凍結集合在凍結狀態下遇到該方法時會拋出InvalidOperationException異常。
NotSupportedException一個越來越重要的子類是PlatformNotSupportedException。該異常表示,操作可以在某些運行環境裡進行,但不能在其他環境裡進行。例如,當將代碼從.NET移植到UWP或.NET Core時,你可能需要使用這個異常,因為它們沒有提供.NET Framework的所有特性。
難以捉摸的FormatException
微軟在設計.NET的第一個版本時犯了一些錯誤。例如,從邏輯上講,FormatException是一個參數異常類型,甚至文檔也說“該異常是在參數格式無效時拋出”。但是,不管出於什麼原因,它實際上沒有繼承ArgumentException。它也沒有地方存放參數名稱。
我們暫時提供的建議是不要拋出FormatException異常,而是自己建立ArgumentException的子類,可以命名為“ArgumentFormatException”或其他效果類似的名稱。這可以為你提供必要的資訊,如參數名稱和實際使用的值,減少調試時間。
這把我們帶回了最初的主題“異常設計”。是的,當你自行開發的解析器檢測到了問題,你可以只拋出一個FormatException異常,但那無法為想要使用你的庫的應用程式開發人員提供協助。
有關這個架構設計缺陷,另外一個例子是IndexOutOfRangeException。從語義上講,它和ArgumentOutOfRangeException沒什麼不同,然而,這個特例只是針對數組索引器嗎?不,那樣想就錯了。看下IList.Item的執行個體集,該方法只會拋出ArgumentOutOfRangeException異常。
環境缺陷
環境缺陷源於世界並不完美這樣一個事實,諸如資料宕機、Web伺服器無響應、檔案丟失等情境。當Bug報告中出現環境缺陷時,需要考慮以下兩個方面:
應用程式正確地處理了缺陷嗎?
在這個環境裡,是什麼導致了缺陷?
通常,這會涉及人員分工。首先,應用程式開發人員應該第一個尋找問題的答案。這不僅僅是說要處理錯誤並恢複,而且要產生一個有用的日誌。
你可能想知道,為什麼要從應用程式開發人員開始。應用程式開發人員要對營運團隊負責。如果一次Web伺服器調用失敗,則應用程式開發人員不能只是甩手大叫“不是我的問題”。他或她首先需要確保異常提供了足夠的細節資訊,讓營運人員可以開展他們的工作。如果異常僅僅提供了“伺服器連線逾時”的資訊,那麼他們怎麼能知道涉及了哪台伺服器?
專用異常
NotImplementedException
NotImplementedException表示且僅表示一件事:這項特性還在開發過程中。因此,NotImplementedException提供的資訊應該總是包含一個任務跟蹤軟體的引用。例如:
throw new NotImplementedException("參見工單#42.");
你可以提供更詳細的資訊,但實際上,你記錄的任何資訊幾乎立刻就會到期。因此,最好是只將讀者導向工單,他們可以在那裡看到諸如該特性按計劃將會在何時實現這樣的資訊。
AggregateException
AggregateException是必要之惡,但很難使用。它本身不包含任何有價值的資訊,所有的細節資訊都隱藏在它的InnerExceptions集合中。
由於AggregateException通常只包含一個項,所以在庫中將它解鎖裝並返回真正的異常似乎是合乎邏輯的。一般來說,你不能在沒有銷毀原始堆疊追蹤的情況下再次拋出一個內部異常,但從.NET 4.5開始,該架構提供了使用ExceptionDispatchInfo的方法。
解鎖裝AggregateException
catch (AggregateException ex){ if (ex.InnerExceptions.Count == 1) //解鎖裝 ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //我們真的需要AggregateException}
無法回答的情況
有一些異常無法簡單地納入這個主題。例如,AccessViolationException表示讀取非託管記憶體時有問題。對,那可能是由原生庫代碼所導致的,也可能是由應用程式錯誤地使用了同樣的程式碼程式庫所導致的。只有通過研究才能揭示這個Bug的本質。
如果可能,你就應該在設計時避免無法回答的異常。在某些情況下,Visual Studio的靜態程式碼分析器甚至可以分析該規則所涵蓋的標識衝突。
例如,ApplicationException實際上已經廢棄。Framework設計指南明確指出,“不要拋出或繼承ApplicationException。”為此,應用程式不必拋出ApplicationException異常。雖說初衷如此,但看下下面這些子類:
Microsoft.JScript.BreakOutOfFinally
Microsoft.JScript.ContinueOutOfFinally
Microsoft.JScript.JScriptException
Microsoft.JScript.NoContextException
Microsoft.JScript.ReturnOutOfFinally
System.Reflection.InvalidFilterCriteriaException
System.Reflection.TargetException
System.Reflection.TargetInvocationException
System.Reflection.TargetParameterCountException
System.Threading.WaitHandleCannotBeOpenedException
顯然,這些子類中有一些應該是參數異常,而其他的則表示環境問題。它們全都不是“應用程式異常”,因為他們只會被.NET Framework的庫拋出。
同樣的道理,開發人員不應該直接使用SystemException。同ApplicationException一樣,SystemException的子類也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微軟甚至建議忘掉SystemException的存在,而只使用其子類。
無法回答的情況有一個子類別,就是基礎設施異常。我們已經看過AccessViolationException,以下是其他的基礎設施異常:
這些異常通常很難診斷,可能會揭示出庫或調用它的代碼中存在的難以理解的Bug。因此,和ApplicationException不同,把它們歸為無法回答的情況是合理的。
實踐:重新設計SqlException
請記住這些原則,讓我們看下SqlException。除了網路錯誤(你根本無法到達伺服器)外,在SQL Server的master.dbo.sysmessages表中有超過11000個不同的錯誤碼。因此,雖然該異常包含了你需要的所有底層資訊,但是,除了簡單地捕獲&記錄外,你實際上難以做任何事。
如果我們要重新設計SqlException,那麼我們會希望,根據我們期望使用者或開發人員做什麼,將其分解成多個不同的類別。
SqlClient.NetworkException會表示所有說明資料庫伺服器本身之外的環境存在問題的錯誤碼。
SqlClient.InternalException會包含說明伺服器存在嚴重故障(如資料庫損壞或無法訪問硬碟)的錯誤碼。
SqlClient.SyntaxException相當於我們的ArgumentException。它是指你向伺服器傳遞了糟糕的SQL(直接或者因為ORM的Bug)。
SqlClient.MissingObjectException會在文法正確但資料庫物件(表、視圖、預存程序等)不存在時出現。
SqlClient.DeadlockException出現在兩個或多個進程試圖修改相同的資訊產生衝突時。
這些異常中的每一種都隱含著一個行動方案。
SqlClient.NetworkException:重試操作。如果頻繁出現,則請聯絡營運人員。
SqlClient.InternalException:立即聯絡DBA。
SqlClient.SyntaxException:通知應用程式或資料庫開發人員。
SqlClient.MissingObjectException:請營運人員檢查上一次資料庫部署是否丟了東西。
SqlClient.DeadlockException:重試操作。如果頻繁發生,則尋找設計錯誤。
如果要在實際的工作中這樣做,那麼我們必須將所有11000多個SQL Server錯誤碼映射到那些類別中的一個,這是一項特別令人望而生畏的工作,這也就解釋了為什麼SqlException是現在這個樣子。
總結
當設計API時,為了便於糾正問題,要將異常根據需要執行的動作的類型進行組織。這樣更容易編寫出自校代碼,記錄更準確的的日誌,更快地將問題傳達給合適的人或團隊。
關於作者
Jonathan Allen在90年代末開始參與面向醫務室的MIS項目,把它們從Access和Excel逐步提升為一種企業級的解決方案。他花了五年時間編寫金融行業自動交易系統,然後決定轉向高端使用者介面開發。在業餘時間裡,他喜歡學習15到17世紀之間的西方格鬥技巧,並進行相關寫作。
以上就是.NET異常設計原則的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!