異常是使用.NET時必然會遇到的問題,但是,有太多的開發人員沒有從API設計的角度考慮這個問題。在大部分工作中,他們自始至終都知道需要捕獲什麼異常以及哪些異常需要寫入全域日誌。如果你設計了可以讓你正確使用異常的API,則可以顯著減少修復缺陷的時間。
誰的錯?
異常設計背後的基本理論始於這樣一個問題,「誰的錯?」為了方便本文的討論,這個問題的答案將總是以下三者之一:
庫
應用程式
環境
當我們說「庫」有問題,我們是指目前執行的某個方法有內部缺陷。在這種情況下,「應用程式」是呼叫庫方法的程式碼(這有點混雜難分,因為函式庫和應用程式程式碼可能在相同的程式集中。)最後,「環境」是指應用程式之外一切無法控制的東西。
函式庫缺陷
最典型的函式庫缺陷是NullReferenceException。對庫而言,它沒有任何理由拋出可以被應用程式偵測到的空引用異常。如果遇到了空,則庫程式碼應該總是拋出一個更具體的異常,說明什麼為空以及如何修正這個問題。對於參數而言,這顯然是一個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.
System.Text.EncoderFallbackException
System.Text.EncoderFallbackExceptionSystem.Text.EncoderFallbackException
System.Text.EncoderFallbackExceptionSystem.Text.恩。那條語句的兩個部分都很重要。考慮下面的程式碼:
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,以下是其他的基础设施异常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的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異常設計原則的內容,更多相關內容請關注PHP中文網(www.php.cn)!