.NET框架源码解析

所有开发人员都应该了解的关于运行时异常的知识(上)

2016-05-21  本文已影响282人  懿民

日期: 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的一个参数;它是三个预定义的宏,并可以看成“异常的性格”。下面是这些宏的解释:

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宏就是用来抛出相同异常的。

上一篇下一篇

猜你喜欢

热点阅读