這種處理的有效性主要取決于所選擇的語言和平台,是以,詳細了解它們的正确用法和行為非常重要,這樣我們的使用者和其他開發人員在診斷代碼中的問題時免受痛苦。
在本文中,我們将了解C和.NET在錯誤處理方面的作用。
詞彙表
CLR:公共語言運作時的縮寫,是.NET運作時,它負責執行用所有.NET語言編譯的應用程式。除了虛拟機和實時編譯器之外,它還具有額外的職責,如記憶體管理、安全性等。
BCL:Base Class Library的縮寫,是.NET framework的核心庫。除了直接使用CLR操作之外,它還公開了原始資料類型和建構和運作應用程式的基本功能。也稱為mscorlib。
FCL:Framework類庫的縮寫,是我們大多數人在.NET中所知道的“架構”。使用BCL作為建構塊,它公開了大量具有各種特性的名稱空間,比如系統IO, 系統安全, 系統文本,等等。
TPL:Task Parallel Library的縮寫,是一個包含由異步關鍵字和API提供的功能的庫。它是随着.NET版本4.5和C#5一起釋出的。
SEH:Structured Exception Handling的縮寫,是Windows的原生異常子系統,它在作業系統級别處理軟體和硬體異常
MDA:托管調試助手的縮寫。這些是特殊的調試擴充,向VisualStudio調試器提供與CLR執行狀态相關的資訊,後者由内部助手和資産公開。
在.NET中,尤其是在C#中,異常是使用try、catch和finally塊來處理的。首先,try塊将包含預期引發異常的代碼,其次是catch塊,它将指定異常類型和在try塊内引發與指定類型比對的異常時将執行的代碼塊:
值得注意的是,異常類型catch将處理所有可能的異常(這并不完全正确,但我們現在将堅持這個假設):
編寫通用異常處理程式的另一種方法是:
try塊可以有多個catch塊,這些catch塊與不同的異常類型相關聯,這些異常塊将根據類層次結構按降序進行求值:
注意:記住異常類型的求值順序很重要,因為如果我們的第一個catch處理一個異常類型,那麼下面所有的異常處理程式都将被忽略。這是一個常見的錯誤,以後在複雜的應用程式中可能很難檢測到。
最後,我們有finally塊(請原諒備援),它将始終執行其包含的代碼,無論是否抛出異常。是以,完整的異常處理程式具有以下形式:
值得注意的是,finally塊中的代碼不包括在異常進行中,是以在這裡抛出的任何異常都不會被捕捉到,并且它會在調用堆棧中冒泡,直到它被管理為止。最後一個細節是,throw關鍵字在異常處理上下文中具有特殊的含義,因為它還允許我們指定如何傳播捕獲到的異常。基于前面的示例:
我們可以在catch塊内以多種方式傳播異常:
catch(DivideByZeroException ex){
1 – Continue with the exception while preserving its stack trace, also called rethrow
throw;
1
2 – Throw the same type of exception but eliminating its original stack trace
throw ex;
throwex;
3 – Throw a new exception, eliminating the original stack trace
throw new InvalidOperationException();
thrownewInvalidOperationException();
4 – Throw a new exception, passing the current one as its InnerException
throw new InvalidOperationException(ex);
}
2
3
thrownewInvalidOperationException(ex);
C#中async的一個已知限制是await關鍵字不能在catch和finally塊中使用。這個問題在C#6中得到了解決,而且,既然我們讨論的是C#6,我們将提到一個新的特性,即catch塊中添加的異常過濾器。這種新的文法糖讓我們根據表達式的布爾值捕獲o傳播異常:
其中一個主要的優點是我們可以在不改變堆棧跟蹤的情況下管理異常流。它還可以用作攔截器,以添加日志等副作用:
我們仍然需要記住之前看到的例外順序,以避免出現以下問題:
下表顯示了BCL中所有基本異常的清單
Exception type
Base type
Description
Exception
Object
Base class for all exceptions.
SystemException
Base class for all runtime-generated errors.
IndexOutOfRangeException
Thrown by the runtime only when an array is indexed improperly.
NullReferenceException
Thrown by the runtime only when a null object is referenced.
AccessViolationException
Thrown by the runtime only when invalid memory is accessed.
InvalidOperationException
Thrown by methods when in an invalid state.
ArgumentException
Base class for all argument exceptions.
ArgumentNullException
Thrown by methods that do not allow an argument to be null.
ArgumentOutOfRangeException
Thrown by methods that verify that arguments are in a given range.
ExternalException
Base class for exceptions that occur or are targeted at environments outside the runtime.
COMException
Exception encapsulating COM HRESULT information.
SEHException
Exception encapsulating Win32 structured exception handling information.
我們還可以包括執行任務和異步代碼(TPL和Parallel LINQ)産生的AggregateException類型。它是一種特殊的異常類型,因為它将多個異常合并到一個對象中,組成一個異常樹。有關詳細資訊,請參閱MSDN上的tasks和PLINQ文章
在C#中,有幾個關鍵字在生成的代碼中包含異常處理:
Using:生成一個try/finally,其中執行包含對象的Dispose方法。
Async/await:異步代碼被編譯到一個狀态機中,該狀态機管理方法調用轉換和委托,除其他外,它還生成catch塊,聚集所有抛出的異常。
Yield:與async類似,coroutines(通過Yield return語句在C中實作)還生成一個狀态機及其各自的異常處理代碼。
自.NET4.5以來,作為TPL發行版的一部分,新特性之一是異常分派,它由命名空間的ExceptionDispatchInfo類提供System.Runtime.ExceptionServices.通過這個類,我們可以保留一個異常并将其委托給另一個執行個體,隻要我們保持在同一個AppDomain中。稍後,我們可以檢查異常并再次抛出它(rethrow):
到目前為止,我們一直假設無論發生什麼,所有異常都将被它們關聯的catch塊無條件地捕獲,然後,最後一個塊将被執行。這還是真的嗎?答案是否定的,至少不總是這樣。根據設計,CLR有一些無法捕獲的特殊異常類型,或者至少在不通過配置強制捕獲的情況下無法捕獲它們。
第一種是損壞狀态異常(CSE)。它們由一組來自Win32/SEH的8個本機異常組成,由于它們的性質,它們假定它們不可管理,因為它們暗示程式處于無效、不一緻和不可恢複的狀态。
本機異常及其CLR對應項如下:
EXCEPTION_ACCESS_VIOLATION – System.AccessViolationException
EXCEPTION_STACK_OVERFLOW – System.StackOverflowException
EXCEPTION_ILLEGAL_INSTRUCTION – SEHException
EXCEPTION_IN_PAGE_ERROR – SEHException
EXCEPTION_INVALID_DISPOSITION – SEHException
EXCEPTION_NONCONTINUABLE_EXCEPTION – SEHException
EXCEPTION_PRIV_INSTRUCTION – SEHException
STATUS_UNWIND_CONSOLIDATE – SEHException
在此執行個體中,異常顯示為“未處理的異常”,并且在visualstudio的本地調試中無法觀察到這些異常。更詳細地診斷這些問題的唯一方法是使用其SOS擴充和分析崩潰轉儲等。
它們被托管代碼忽略的原因是,它們是來自本機代碼的傳入異常,它們逃避了應用程式的責任,或者,我們正在處理一個CLR錯誤,這種情況不太可能,但并非不可能。如果我們使用C的不安全特性來編寫非托管代碼(直接通路記憶體),也可能是代碼中的錯誤造成的。
如果我們仍然希望處理這些異常,CLR讓我們可以選擇通過屬性HandleProcessCorruptedStateExceptions捕捉其中的一些(而不是全部)。這要求方法具有SecurityCritical的通路級别:
在我們的應用程式配置中,也可以通過将legacyCorruptedStateExceptionPolicy屬性指定為true來實作。
注意:由于相容性的原因,CSE将在即将到來的.netcore版本中被删除。由于桌面版本是Windows的原始版本,是以在桌面版本中這些内容将保持不變。
還有兩種無法處理的異常類型,它們表示程序過早終止且不可恢複:StackOverflowExceptions(如果源于CLR)和CLR異常,我們将在下面看到。
深入研究異常的起源,我們偶然發現了CLR異常。運作時主要是用C++建構的,并且具有支配複雜場景的任務,即處理多個異常類型:
本機Windows異常(SEH):CLR将這些異常的處理與模拟VC++編譯器内部函數的宏統一起來(“uu try”、“uu catch”等)。SEH模型非常複雜,例如,與Linux相比,在堆棧幀和二進制相容性方面,展開(我們将在下一節中看到)是一個代價高昂的過程。
C++異常:這些異常可以由CLR代碼本身引發。
CLR異常:這些異常是在執行期間由虛拟機委派并向應用程式公開的異常。
corecrr是CLR的多平台和開源版本,它的任務更艱巨,那就是內建與多種平台和架構(Linux和macx64,以及其他正在開發的x86和ARM32/ARM64)的相容性。為了達到所需的解耦級别,它依賴于一個稱為PAL(平台适配層)的層。
如果CLR中發生嚴重錯誤,它将抛出ExecutionEngineException(不推薦)和fataxecutionengineerror(MDA)。這些錯誤通常表示托管代碼中的堆損壞。
為了完成異常之旅,我們将繼續進行簡短但必須讨論的性能含義。與普通代碼的執行相比,異常處理非常昂貴。以下行為和副作用會導緻異常情況:
由于執行流(以及上下文)的改變,在記憶體中緩存錯誤和頁面錯誤。
展開:這是在try塊中屬于導緻異常的代碼的上下文被“清理”的過程。這涉及到周遊所有以前的堆棧幀以及釋放/處置受影響的對象。此外,所有這些都可能導緻垃圾回收器的下一次記憶體壓縮。
診斷對象的附加配置設定(StackTrace)
“冷”代碼通路,這需要額外的時間進行即時編譯(JIT)。
為了完成本文,我們将提供一些在設計和實施有效解決方案時有用的提示:
避免在應用程式中使用異常作為控制流。使用錯誤代碼,或者更好的是,将錯誤和警告作為類設計的一部分來考慮。
以異常抛出和捕獲來衡量:例如,在web場景中,異常處理可以集中在每個請求上,或者,在分層體系結構中,可以跨層對每次調用進行處理(依賴注入架構為此提供了幾種工具)。
避免使用過多代碼的try塊,尤其是當抛出的代碼不是立即相關或依賴于它時。這是因為大型代碼體(在方法和塊中)可以搶先地禁用由實時編譯器(JIT)進行的運作時優化