CLR允许异常抛出任何类型的实例,从Int32到String都可以。但是,Microsoft决定不强迫所有编程语言都抛出和捕捉任何类型的异常。因此,他们定义了System.Exception类型,并规定所有CLS相容的编程语言都必须能抛出和捕捉该类型的异常。派生自System.Exception的异常类型被认为是CLS相容的。C#和其他许多编程语言的编译器都只允许代码抛出CLS相容的异常。
System.Exception是一个非常简单的类型,它包含了一些属性。但是,一般不要写任何代码以任何方式查询或访问这些属性,相反,当应用程序由于一个未处理的异常而终止时,可以在调试器中查看这些属性,或者在Window应用程序事件日志或崩溃转储中查看。
这里有必要讲一下System.Exception异常提供的只读属性StackTrace.catch块可读取该属性来获取一个堆栈跟踪,它描述了异常发生前调用的方法。为了检查异常原因并修改代码,堆栈跟踪非常有用。访问该属性时,实际要调用CLR中的代码;该属性并不是简单返回一个字符串。构造一个Exception派生类的一个新对象时,StackTrace属性被初始化为null.如果此时读取该属性,得到的不是堆栈跟踪,而是一个null。
一个异常抛出时,CLR会在内部记录throw指令的位置(抛出位置)。一个catch块捕捉到该异常时,CLR又会记录异常的捕捉位置。在catch块内访问被抛出的异常对象的StackTrace属性,负责实现该属性的代码会调用CLR内部的代码,后者创建一个字符来指出从异常抛出位置到异常捕捉位置的所有方法。
以下代码抛出和它捕捉的一样的异常对象,导致CLR重置该异常的起点:
Private void SomeMethod()
{
try{}
catch(Exception e)
{throw e;}//CLR认为这是异常的起始点。
}
相反,如果仅仅使用throw关键词本身(删除后面的e)来重新抛出一个异常对象,CLR就不会重置堆栈的起点。以下代码重新抛出它捕捉到的异常,但不会导致CLR重置起点。
Private void SomeMethod()
{
try{}
catch(Exception e)
{throw ;}//不影响CLR对异常起点的认识
}
实际上,上述两段代码的唯一区别就是CLR对异常的起始抛出位置的认知。令人遗憾的是,抛出或重新抛出一个异常,Window会重置堆栈起点。因此,如果一个异常成为一个未处理的异常,那么向Windows Error Reporting报告的堆栈位置就是最后一次抛出或重新抛出的位置。之所以令人遗憾,是因为假如应用程序在字段哪里失败会使调试工作变的异常困难。有一些开发人员无法忍受这一点,于是选择以一种不同的方式来实现代码,确保堆栈跟踪能真正返回一个异常的原始抛出位置:
Private void SomeMethod()
{
Boolean trySucceeds=false;
Try
{
trySucceeds=true;
}
Finally
{
If(!trySucceeds)
}
}
在StackTrace属性返回的字符串中,不包含调用栈中比接受异常对象的那个catch块高的任何方法。要获得从线程起始处到异常程序之间的完整堆栈跟踪,需要使用System.Diagnostics.StackTrace对象。该类型定义了一些属性和方法,允许开发人员程序化的处理堆栈跟踪以及构造堆栈跟踪的栈帧。
可能有几个不同的构造器来构造一个StackTrace对象。一些构造器构造从线程起始处到StackTrace对象的构造位置的栈帧。另一些构造器使用作为参数传递的一个Exception派生对象来初始化栈帧。
如果CLR能找到你的程序集的调试符号(存储在pdb中),那么在System.Exception的StackTrace属性或者System.Diagnostics.StackTrace的ToString方法返回的字符串中,将包含源代码文件路径和代码行号,这些信息对于调试时非常有用的。
获得一个堆栈跟踪后,可能发现在实际调用栈中有的一些方法没有出现在堆栈跟踪字符串中。这是由于两种原因造成的。首先,调用栈其实是线程返回位置的一个记录。
其次,JIT编译器可能内联一些方法,从而避免了调用一个独立的方法并从中返回的开销。许多编译器都使用了/Debug命令行开关。使用这个开关,编译器会在生成的程序集中嵌入信息,告诉JIT编译器不要内链程序集的任何方法,确保调试人员获得一个更完整、更有意义的堆栈跟踪。