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