所有开发人员都应该了解的关于运行时异常的知识(上)
日期: 2005
当我们提到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_TRY
// 调用一些函数,也许会抛出一个异常
Bar();
EX_CATCH
// 在这里,那就有错误发生了
m_finalDisposition = terminallyHopeless;
EX_END_CATCH(RethrowTransientExceptions)
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();
通过对应的托管异常对象获取更多信息是更便捷的办法。如果异常要传递到托管代码,无论是即时还是被捕捉稍后处理,都是需要这个托管对象的。而且这个异常对象也很容易读取,其是一个托管的objectref引用,因此可以用常规办法:
OBJECTREF throwable = NULL;
GCPROTECT_BEGIN(throwable);
// . . .
EX_TRY
// . . . do something that might throw
EX_CATCH
throwable = GET_THROWABLE();
EX_END_CATCH(RethrowTransientExceptions)
// . . . do something with throwable
GCPROTECT_END()
有时,虽然是异常实现的底层,无法避免要用到C++异常对象。如果C++异常的类型很重要,也有一些轻量级的RTTI函数来帮助归类异常,如:
Exception *pEx = GET_EXCEPTION();
if (pEx->IsType(CLRException::GetType())) {/* ... */}
可以反馈一个异常是否是(或继承自)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;
EX_TRY
// code
EX_CATCH_HRESULT (hr)
return hr;
然而,虽然很诱人,但不总是正确的。EX_CATCH_HRESULT捕捉所有的异常,保存HRESULT,并丢掉原始异常。因此,除非丢掉异常这个行为是函数所需要的,否则EX_CATCH_HRESULT并不是很合适。
EX_RETHROW
如上所述,异常宏捕捉所有异常;捕捉一个指定异常的唯一办法是先捕捉所有的异常,再将除了要捕捉的其它异常再次抛出。因此,当一个异常被捕捉,处理之后,结果其不是要被捕捉的,那它可能会被重新抛出。EX_RETHROW宏就是用来抛出相同异常的。