.NET异常设计原则
異常是使用.NET時必然會遇到的問題,但是,有太多的開發(fā)人員沒有從API設(shè)計的角度考慮這個問題。在大部分工作中,他們自始至終都知道需要捕獲什么異常以及哪些異常需要寫入全局日志。如果你設(shè)計了可以讓你正確使用異常的API,則可以顯著減少修復(fù)缺陷的時間。
誰的錯?
異常設(shè)計背后的基本理論始于這樣一個問題,“誰的錯?”為了方便本文的討論,這個問題的答案將總是以下三者之一:
庫
應(yīng)用程序
環(huán)境
當我們說“庫”有問題,我們是指當前執(zhí)行的某個方法有內(nèi)部缺陷。在這種情況下,“應(yīng)用程序”是調(diào)用庫方法的代碼(這有點混雜難分,因為庫和應(yīng)用程序代碼可能在相同的程序集中。)最后,“環(huán)境”是指應(yīng)用程序之外一切無法控制的東西。
庫缺陷
最典型的庫缺陷是NullReferenceException。對庫而言,它沒有任何理由拋出可以被應(yīng)用程序檢測到的空引用異常。如果遇到了空,則庫代碼應(yīng)該總是拋出一個更具體的異常,說明什么為空以及如何糾正這個問題。對于參數(shù)而言,這顯然是一個ArgumentNullException異常。而如果屬性或字段為空,則InvalidOperationException通常更合適。
根據(jù)定義,任何表明庫缺陷的異常都是該庫中需要修復(fù)的Bug。那并不是說應(yīng)用程序代碼沒有Bug,而是說庫的Bug需要首先修復(fù)。只有那樣,才能讓應(yīng)用程序開發(fā)人員知道他也犯了錯誤。
這樣做的原因是,可能有許多人使用同樣的庫。如果一個人在不應(yīng)該傳入空的地方錯誤地傳入了空,則其他人想必也會犯同樣的錯誤。把NullReferenceException替換為一個可以清晰地顯示出什么出錯的異常,應(yīng)用程序開發(fā)人員立即就可以知道什么出錯了。
“成功之核(The Pit of Success)”
如果你讀過有關(guān).NET設(shè)計模式的早期文獻,那么你會經(jīng)常碰到短語“成功之核”。其基本思想是這樣的:讓代碼容易被正確使用,不容易被誤用,并確保異??梢愿嬖V你哪里出錯了。遵循這個API設(shè)計理念,幾乎可以保證開發(fā)人員一開始就編寫出正確的代碼。
這就是為什么一個沒有注釋的NullReferenceException是如此糟糕。除了堆棧跟蹤外(可能非常深入庫代碼),沒有任何信息可以幫助開發(fā)人員確定他們哪里做錯了。另一方面,ArgumentNullException和InvalidOperationException則為庫作者提供了一種方法,讓他們可以向應(yīng)用程序開發(fā)人員說明如何修復(fù)問題。
其他庫缺陷
下一個庫缺陷是ArithmeticException系列,包括DivideByZeroException、FiniteNumberException和OverflowException。再次,這總是意味著庫方法的內(nèi)部缺陷,即使那個缺陷只是一個缺失的參數(shù)有效性檢查。
庫缺陷的另外一個例子是IndexOutOfRangeException。從語義上講,它和ArgumentOutOfRangeException沒什么不同,參見IList.Item,但它只適用于數(shù)組索引器。由于應(yīng)用程序代碼通常不會使用裸數(shù)組,所以這意味著,自定義的集合類會有Bug。
自.NET 2.0引入泛型列表以來,ArrayTypeMismatchException就很少見了。觸發(fā)該異常的情況相當怪異。根據(jù)文檔:
當系統(tǒng)無法將數(shù)組元素轉(zhuǎn)換成聲明的數(shù)組類型時會拋出ArrayTypeMismatchException。例如,一個String類型的元素無法存入一個Int32數(shù)組,因為這兩種類型之間無法轉(zhuǎn)換。應(yīng)用程序一般是不需要拋出這類異常的。
要做到這一點,前面提到的Int32數(shù)組必須存入一個Object[]類型的變量。如果你使用了原始數(shù)組,則庫需要對此進行檢查。由于這個原因及其他許多方面的考慮,最好是不要使用原始數(shù)組,而是將它們封裝到一個合適的集合類中。
通常,其他轉(zhuǎn)換問題是通過InvalidCastException異常反映出來的?;氐轿覀兊闹黝},類型檢查應(yīng)該意味著永遠不會拋出InvalidCastException異常,而是向調(diào)用者拋出ArgumentException或InvalidOperationException異常。
MemberAccessException是一個基類,涵蓋了各種基于反射的錯誤。除了直接使用反射外,COM互操作和動態(tài)關(guān)鍵詞的不正確使用都會觸發(fā)該異常。
應(yīng)用程序缺陷
典型的應(yīng)用程序缺陷是ArgumentException及其子類ArgumentNullException和ArgumentOutOfRangeException。以下是其他你可能不知道的子類:
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
所有這些都明確地表明應(yīng)用程序有錯誤,而問題就出在調(diào)用庫方法的行里。那條語句的兩個部分都很重要。考慮下面的代碼:
foo.Customer = null; foo.Save();如果上述代碼拋出了一個ArgumentNullException異常,那么應(yīng)用程序開發(fā)人員會很困惑。它應(yīng)該拋出一個InvalidOperationException異常,說明當前行之前有什么地方出了問題。
以異常為文檔
典型的程序員不閱讀文檔,至少不會首先閱讀文檔。相反,他或她會閱讀公共API,編寫一些代碼并運行。如果代碼不能正常運行,就到Stack Overflow上搜索異常信息。如果該程序員夠幸運,則很容易在那里找到答案以及指向正確文檔的鏈接。但即使如此,程序員們很可能也不會真正地讀它。
那么,作為庫作者,我們?nèi)绾谓鉀Q這個問題?第一步是直接將部分文檔復(fù)制到異常中。
更多對象狀態(tài)異常
InvalidOperationException有一個眾所周知的子類ObjectDisposedException。它的用途顯而易見,然而,很少有可銷毀類會忘記拋出這個異常。如果忘記了,則常見的結(jié)果是拋出NullReferenceException異常。該異常是由Dispose方法將可銷毀子對象置為空所導(dǎo)致的。
與InvalidOperationException密切相關(guān)的是NotSupportedException異常。這兩種異常很容易區(qū)分:InvalidOperationException是指“你現(xiàn)在不能那樣操作”,而NotSupportedException是指“你永遠不能對這個類做那種操作”。理論上講,NotSupportedException應(yīng)該只在使用抽象接口時出現(xiàn)。
例如,一個不可變集合在遇到IList.Add方法時應(yīng)該拋出NotSupportedException異常。相比之下,一個可凍結(jié)集合在凍結(jié)狀態(tài)下遇到該方法時會拋出InvalidOperationException異常。
NotSupportedException一個越來越重要的子類是PlatformNotSupportedException。該異常表示,操作可以在某些運行環(huán)境里進行,但不能在其他環(huán)境里進行。例如,當將代碼從.NET移植到UWP或.NET Core時,你可能需要使用這個異常,因為它們沒有提供.NET Framework的所有特性。
難以捉摸的FormatException
微軟在設(shè)計.NET的第一個版本時犯了一些錯誤。例如,從邏輯上講,FormatException是一個參數(shù)異常類型,甚至文檔也說“該異常是在參數(shù)格式無效時拋出”。但是,不管出于什么原因,它實際上沒有繼承ArgumentException。它也沒有地方存放參數(shù)名稱。
我們暫時提供的建議是不要拋出FormatException異常,而是自己創(chuàng)建ArgumentException的子類,可以命名為“ArgumentFormatException”或其他效果類似的名稱。這可以為你提供必要的信息,如參數(shù)名稱和實際使用的值,減少調(diào)試時間。
這把我們帶回了最初的主題“異常設(shè)計”。是的,當你自行開發(fā)的解析器檢測到了問題,你可以只拋出一個FormatException異常,但那無法為想要使用你的庫的應(yīng)用程序開發(fā)人員提供幫助。
有關(guān)這個框架設(shè)計缺陷,另外一個例子是IndexOutOfRangeException。從語義上講,它和ArgumentOutOfRangeException沒什么不同,然而,這個特例只是針對數(shù)組索引器嗎?不,那樣想就錯了。看下IList.Item的實例集,該方法只會拋出ArgumentOutOfRangeException異常。
環(huán)境缺陷
環(huán)境缺陷源于世界并不完美這樣一個事實,諸如數(shù)據(jù)宕機、Web服務(wù)器無響應(yīng)、文件丟失等場景。當Bug報告中出現(xiàn)環(huán)境缺陷時,需要考慮以下兩個方面:
應(yīng)用程序正確地處理了缺陷嗎?
在這個環(huán)境里,是什么導(dǎo)致了缺陷?
通常,這會涉及人員分工。首先,應(yīng)用程序開發(fā)人員應(yīng)該第一個查找問題的答案。這不僅僅是說要處理錯誤并恢復(fù),而且要生成一個有用的日志。
你可能想知道,為什么要從應(yīng)用程序開發(fā)人員開始。應(yīng)用程序開發(fā)人員要對運維團隊負責。如果一次Web服務(wù)器調(diào)用失敗,則應(yīng)用程序開發(fā)人員不能只是甩手大叫“不是我的問題”。他或她首先需要確保異常提供了足夠的細節(jié)信息,讓運維人員可以開展他們的工作。如果異常僅僅提供了“服務(wù)器連接超時”的信息,那么他們怎么能知道涉及了哪臺服務(wù)器?
專用異常
NotImplementedException
NotImplementedException表示且僅表示一件事:這項特性還在開發(fā)過程中。因此,NotImplementedException提供的信息應(yīng)該總是包含一個任務(wù)跟蹤軟件的引用。例如:
throw new NotImplementedException("參見工單#42.");你可以提供更詳細的信息,但實際上,你記錄的任何信息幾乎立刻就會過期。因此,最好是只將讀者導(dǎo)向工單,他們可以在那里看到諸如該特性按計劃將會在何時實現(xiàn)這樣的信息。
AggregateException
AggregateException是必要之惡,但很難使用。它本身不包含任何有價值的信息,所有的細節(jié)信息都隱藏在它的InnerExceptions集合中。
由于AggregateException通常只包含一個項,所以在庫中將它解封裝并返回真正的異常似乎是合乎邏輯的。一般來說,你不能在沒有銷毀原始堆棧跟蹤的情況下再次拋出一個內(nèi)部異常,但從.NET 4.5開始,該框架提供了使用ExceptionDispatchInfo的方法。
解封裝AggregateException
catch (AggregateException ex) {???? if (ex.InnerExceptions.Count == 1) //解封裝???????
? ? ? ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();??
? else?????
? ?? throw; //我們真的需要AggregateException }
無法回答的情況
有一些異常無法簡單地納入這個主題。例如,AccessViolationException表示讀取非托管內(nèi)存時有問題。對,那可能是由原生庫代碼所導(dǎo)致的,也可能是由應(yīng)用程序錯誤地使用了同樣的代碼庫所導(dǎo)致的。只有通過研究才能揭示這個Bug的本質(zhì)。
如果可能,你就應(yīng)該在設(shè)計時避免無法回答的異常。在某些情況下,Visual Studio的靜態(tài)代碼分析器甚至可以分析該規(guī)則所涵蓋的標識沖突。
例如,ApplicationException實際上已經(jīng)廢棄。Framework設(shè)計指南明確指出,“不要拋出或繼承ApplicationException。”為此,應(yīng)用程序不必拋出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
顯然,這些子類中有一些應(yīng)該是參數(shù)異常,而其他的則表示環(huán)境問題。它們?nèi)疾皇恰皯?yīng)用程序異?!?#xff0c;因為他們只會被.NET Framework的庫拋出。
同樣的道理,開發(fā)人員不應(yīng)該直接使用SystemException。同ApplicationException一樣,SystemException的子類也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微軟甚至建議忘掉SystemException的存在,而只使用其子類。
無法回答的情況有一個子類別,就是基礎(chǔ)設(shè)施異常。我們已經(jīng)看過AccessViolationException,以下是其他的基礎(chǔ)設(shè)施異常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
這些異常通常很難診斷,可能會揭示出庫或調(diào)用它的代碼中存在的難以理解的Bug。因此,和ApplicationException不同,把它們歸為無法回答的情況是合理的。
實踐:重新設(shè)計SqlException
請記住這些原則,讓我們看下SqlException。除了網(wǎng)絡(luò)錯誤(你根本無法到達服務(wù)器)外,在SQL Server的master.dbo.sysmessages表中有超過11000個不同的錯誤代碼。因此,雖然該異常包含了你需要的所有底層信息,但是,除了簡單地捕獲&記錄外,你實際上難以做任何事。
如果我們要重新設(shè)計SqlException,那么我們會希望,根據(jù)我們期望用戶或開發(fā)人員做什么,將其分解成多個不同的類別。
SqlClient.NetworkException會表示所有說明數(shù)據(jù)庫服務(wù)器本身之外的環(huán)境存在問題的錯誤代碼。
SqlClient.InternalException會包含說明服務(wù)器存在嚴重故障(如數(shù)據(jù)庫損壞或無法訪問硬盤)的錯誤代碼。
SqlClient.SyntaxException相當于我們的ArgumentException。它是指你向服務(wù)器傳遞了糟糕的SQL(直接或者因為ORM的Bug)。
SqlClient.MissingObjectException會在語法正確但數(shù)據(jù)庫對象(表、視圖、存儲過程等)不存在時出現(xiàn)。
SqlClient.DeadlockException出現(xiàn)在兩個或多個進程試圖修改相同的信息產(chǎn)生沖突時。
這些異常中的每一種都隱含著一個行動方案。
SqlClient.NetworkException:重試操作。如果頻繁出現(xiàn),則請聯(lián)系運維人員。
SqlClient.InternalException:立即聯(lián)系DBA。
SqlClient.SyntaxException:通知應(yīng)用程序或數(shù)據(jù)庫開發(fā)人員。
SqlClient.MissingObjectException:請運維人員檢查上一次數(shù)據(jù)庫部署是否丟了東西。
SqlClient.DeadlockException:重試操作。如果頻繁發(fā)生,則查找設(shè)計錯誤。
如果要在實際的工作中這樣做,那么我們必須將所有11000多個SQL Server錯誤代碼映射到那些類別中的一個,這是一項特別令人望而生畏的工作,這也就解釋了為什么SqlException是現(xiàn)在這個樣子。
總結(jié)
當設(shè)計API時,為了便于糾正問題,要將異常根據(jù)需要執(zhí)行的動作的類型進行組織。這樣更容易編寫出自校代碼,記錄更準確的的日志,更快地將問題傳達給合適的人或團隊。
關(guān)于作者
Jonathan Allen在90年代末開始參與面向醫(yī)務(wù)室的MIS項目,把它們從Access和Excel逐步提升為一種企業(yè)級的解決方案。他花了五年時間編寫金融行業(yè)自動交易系統(tǒng),然后決定轉(zhuǎn)向高端用戶界面開發(fā)。在業(yè)余時間里,他喜歡學(xué)習(xí)15到17世紀之間的西方格斗技巧,并進行相關(guān)寫作。
原文地址:http://www.infoq.com/cn/articles/Exceptions-API-Design
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的.NET异常设计原则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 配置高性能ElasticSearch集群
- 下一篇: 权衡微服务