所有CLR開發人員都應該了解的關於運行時異常的知識(上)
當我們提到CLR里的「異常」,要注意一個很重要的區別。有通過如C#的try/catch/finally暴露給應用程序,並由運行時提供機制全權實現的託管異常。也有運行時自己使用的異常。大部分運行時開發人員很少需要想到如何實現並暴露託管異常模型。但每個運行時開發人員都應該懂得CLR實現里是怎麼使用異常的。為了保持區分,本文將託管程序拋出並捕捉的稱為託管異常,而將運行時自己使用的錯誤處理方式稱為CLR內部異常。本文主要討論CLR內部異常。
異常在什麼地方有用?
異常幾乎在所有地方都有用。最有用的地方就是拋出或捕捉異常的函數里,因為需要顯式編寫代碼來拋出異常或者捕捉其並優雅的處理異常。即使一個函數本身不拋出異常,它也有可能調用拋出異常的函數。這樣該函數必須在異常拋出的時候行為正常。明智的使用支持物(holders)可以極大簡化正確編寫這類代碼。
為什麼CLR內部異常是不同的?
CLR內部異常更像C++異常,但不完全是。CLR可以在Mac OSX、BSD還有Windows下編譯。操作系統和編譯器的差異使得我們不能僅使用標準C++的try/catch。另外,CLR內部異常還提供了類似託管代碼的「finally」和「fault」這樣的功能。
通過一些宏,編寫異常處理代碼就像標準C++那樣簡單。
捕捉異常
EX_TRY
最基本的宏是:EX_TRY / EX_CATCH / EX_END_CATCH,使用方法如下:
EX_TRYn // 調用一些函數,也許會拋出一個異常n Bar();nEX_CATCH n // 在這裡,那就有錯誤發生了n m_finalDisposition = terminallyHopeless; nEX_END_CATCH(RethrowTransientExceptions)n
EX_TRY宏就是引入try塊,很像C++的「try」,除了其還添加了一個大括弧:「{」。
EX_CATCH
EX_CATCH宏結束一個try塊,並添加一個大括弧:「}」,並且開始catch塊。跟EX_TRY類似,其也添加了一個大括弧來開始catch塊。
這裡和C++異常有很大的不同:CLR開發者根本不明確捕捉什麼。實際上,這些宏捕捉包括類似AV的非C++異常或託管異常的任何東西。如果一塊代碼只需要捕捉一個或者一小部分異常,那麼它需要捕捉並檢查異常,然後將所有不相關的異常再次拋出。
需要再次指明的是EX_CATCH宏捕捉任何東西。這個可能不是一個函數需要的。下兩個章節討論如何處理不應該被捕捉的異常。
GET_EXCEPTION() & GET_THROWABLE()
當一個CLR開發人員捕捉到一個東西,那麼他要如何決定做什麼?取決於需求,有幾個選項:
第一,無論捕捉到什麼(C++)異常,都是繼承自全局的Exception類的類的實例。一些繼承類很明顯,如OutOfMemoryException。另一些則有些領域相關,如EETypeLoadException。還有些類只是系統異常的簡單封裝,如CLRException(包含OBJECTHANDLE欄位指向一個託管異常),或HRException(HRESULT的封裝)。如果最初的異常不是從Exception繼承來的,那麼宏會給其做一個封裝。(注意所有異常都是系統自帶而且眾所周知的)。
第二,每個CLR內部異常都有一個關聯的HRESULT值。有時像HRException那樣,值從某個COM對象來的,但內部異常和Win32 api錯誤值也有HRESULT值。
最後,幾乎所有CLR內部發生的異常都有可能傳遞到託管代碼那邊,CLR內部異常都有跟其對應的託管異常。創建託管異常不是必須的,但是總有辦法獲取它。
那麼,CLR開發人員將如何給一個異常分類呢?
常用的做法是,通過異常關聯的HRESULT值分類,而且有一個很簡單的辦法取值:
HRESULT hr = GET_EXCEPTION()->GetHR();n
通過對應的託管異常對象獲取更多信息是更便捷的辦法。如果異常要傳遞到託管代碼,無論是即時還是被捕捉稍後處理,都是需要這個託管對象的。而且這個異常對象也很容易讀取,其是一個託管的objectref引用,因此可以用常規辦法:
OBJECTREF throwable = NULL;nGCPROTECT_BEGIN(throwable);n// . . .nEX_TRYn // . . . do something that might thrownEX_CATCHn throwable = GET_THROWABLE();nEX_END_CATCH(RethrowTransientExceptions)n// . . . do something with throwablenGCPROTECT_END()n
有時,雖然是異常實現的底層,無法避免要用到C++異常對象。如果C++異常的類型很重要,也有一些輕量級的RTTI函數來幫助歸類異常,如:
Exception *pEx = GET_EXCEPTION();nif (pEx->IsType(CLRException::GetType())) {/* ... */}n
可以反饋一個異常是否是(或繼承自)CLRException。
EX_END_CATCH(RethrowTransientExceptions)
在上面的例子中,「RethrowTransientExceptions」是宏EX_END_CATCH的一個參數;它是三個預定義的宏,並可以看成「異常的性格」。下面是這些宏的解釋:
- SwallowAllExceptions: 命名很簡單巧妙。如名字所示,它吞沒任何對象。顯而易見,通常不是正確的做法。
- RethrowTerminalExceptions: 一個更好的名字應該是"RethrowThreadAbort", 也就是這個宏的作用。
- RethrowTransientExceptions:"臨時"異常的最好定義是,如果重試則該異常在其它環境里有可能不再發生。下面這些是臨時異常:
- COR_E_THREADABORTED
- COR_E_THREADINTERRUPTED
- COR_E_THREADSTOP
- COR_E_APPDOMAINUNLOADED
- E_OUTOFMEMORY
- HRESULT_FROM_WIN32(ERROR_COMMITMENT_LIMIT)
- HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY)
- (HRESULT)STATUS_NO_MEMORY
- COR_E_STACKOVERFLOW
- MSEE_E_ASSEMBLYLOADINPROGRESS
CLR開發人員在不確定的情況下一般應該使用RethrowTransientExceptions.
但在任何情況下,編寫EX_END_CATCH的開發人員都需要考慮捕捉哪些異常,並只捕捉這些異常。而且,因為這個宏捕捉所有的東西,不去捕捉一個異常的唯一方法就是重新拋出它。
如果一個EX_CATCH / EX_END_CATCH塊正確分類異常,並在必要的時候重新拋出,那麼SwallowAllExceptions就是告訴宏不必重新拋出異常的辦法。
EX_CATCH_HRESULT
有的時候需要的就是異常對應的那個HRESULT值,特別是針對COM的代碼。對於這些情況,使用EX_CATCH_HRESULT宏比編寫一個EX_CATCH塊簡單的多。一個典型代碼片段如下:
HRESULT hr;nEX_TRYn // codenEX_CATCH_HRESULT (hr)nnreturn hr;n
然而,雖然很誘人,但不總是正確的。EX_CATCH_HRESULT捕捉所有的異常,保存HRESULT,並丟掉原始異常。因此,除非丟掉異常這個行為是函數所需要的,否則EX_CATCH_HRESULT並不是很合適。
EX_RETHROW
如上所述,異常宏捕捉所有異常;捕捉一個指定異常的唯一辦法是先捕捉所有的異常,再將除了要捕捉的其它異常再次拋出。因此,當一個異常被捕捉,處理之後,結果其不是要被捕捉的,那它可能會被重新拋出。EX_RETHROW宏就是用來拋出相同異常的。
推薦閱讀:
※如何在C#中存儲大量數據而不引發OutOfMemoryException?
※C++與C++/CLI的運行速度相比哪個快? C++/CLI和C#的運行速度一樣快?
※.NET 下的性能問題如何定位?
※如何在幾天之內將數萬行C#代碼移入Flex?
※零基礎新手求推薦C#.net的書?