.NET采用的结构化异常(exception)处理系统是检测和处理系统错误的一种不但高效并且有力的方法。但是,假如你在此以前从来没有处理过异常(比方说像Visual Basic程序员那样——他们看.NET有点犯晕)那么你多少可能会对异常处理存在一点恐惧感,结果可能会促使你犯下“一出错误到处Goto”的大问题。这篇文章就是对.NET结构化异常处理系统的概述,同时我还会解释下,相比采用返回值和错误代码的方法,为什么用异常处理更具优势。
简单地说,异常代表了应用程序的某种反常状态,也就是所谓“异常状况”。通常,这种应用程序中出现的异常会产生某些类型的错误,而且异常自身也会采用某些方式来描述错误的状况。当程序中发生异常时,它会沿着函数调用链传递直到发生以下两种情况:发现了能够处理异常的处理器或者异常被扔出应用程序的主要方法,在这种情况之下就会触发缺省的异常处理器。通常也就意味着结束程序的运行。
毫不奇怪,在面向对象的.NET框架内,异常也是用对象来代表的,异常对象的继承顺序最终可以追溯到System.Exception对象。当错误发生时候我们就说“扔出”了异常,而在错误得到处理时我们则说异常被“捕获”。异常的处理必须采用Try…Catch的编码结构,其最简单的示范形式请参看图A中的代码。
图A
由Try代码块保护的代码所发生的任何异常,甚至包括在不含Try代码块的被调函数或方法以内的异常都将被Catch代码块内的代码处理。当然,除非catch代码块自己也扔出了异常,而在这种情况下异常会被扔到下一个级别更高的try代码块,哪怕那意味着从当前正在执行的函数中扔出异常。
到目前为止所有的一切听起来都还不错:如果在受到处理器保护的代码中发生了错误,我们的错误处理机制会把程序的执行流程转到错误处理代码那里。如果当前正在执行的代码中没有相应的处理器,或者错误就发生在错误处理器内部,那么错误将沿着函数调用堆栈向上传递。然而,恰恰就在这里有个你以前可并不习惯或者还不曾知道的概念,这就是所谓的Finally代码块。
只要发生异常,try语句中的finally代码块所包含的代码就总会执行。是否包括finally是可选的,除非你的try代码块没有对应的catch代码块,也就是说try总要以其他类型的代码块结束,要不是catch,要不是finally。你可以在finally代码块内编写自己希望总是执行的错误清除代码而不用考虑是否会有错误发生。这类代码可能包括关闭文件、数据库连接或其他有限资源的代码。注意,finally代码在catch代码之后立即执行,除非catch扔出了异常。
在用VB6处理错误时,如果你确切地想要弄明白到底发生了怎样的错误,那么你通常不得不采用if或者case结构来检查Err.Number。这经常会产生一种相当复杂的错误处理逻辑,弄不好这些错误处理结构自身都可能错误成堆,有时甚至比它要保护的代码都来得更长、更复杂。
在.NET框架内,考虑到所有的异常都是对象而且因此也是一种可与其他类型相比的新类型,所以你完全可以消除类似Err.Number=5647828312所带来的编程麻烦。Catch语句经过稍加修改就充分利用了这一功能以处理特定的异常类型,如图B显示的那样。
图B
你可以随自己的愿望设置特殊处理或者一般处理的Catch语句,比方说,图B中的第1个Catch语句只捕获System.NullReferenceException类型的异常。而第2个catch语句则只捕获源自System.ArgumentException的异常,而第3个catch语句则会捕获没有被以上两个catch语句所捕获的其他所有异常。在这种情况下,异常被当前的try块扔出并被CLR在调用函数堆栈中找到的对应Catch块处理。我们要指出,这最后一个Catch块是多余的,因为那正是在其没有包括在内的情况下所产生的行为。
这个简短的例子说明了关于异常的4条重要规则:
这些规则不但相当重要而且规定得也算漂亮,这样你的代码就可以选择性地只处理那些有能力处理的错误,同时相信其他问题都会统统交给调用堆栈,哪怕至少作为通知处理。在实际应用中,因为让CLR观察扔出的异常对性能有一定的影响,所以使用单个try块同时对应多个特殊异常的catch语句(正如我们在图B中所看到的那样)是检查代码多个特定错误的最佳方式。
你还能在代码中创建和扔出自己规定的异常,由它发出系统定义错误发生的信号但事实上却并没有发生。所有的异常类都提供了重载的构造器,允许你包括有关异常原因的特定信息。在创建异常时,包括至少一条解释消息通常是个好主意。
你应该能很聪明地想到,假如人们能显式地创建和扔出系统定义的异常那么他也就能创建和扔出自己的异常。这种定制的异常通常就是应用级的错误,是对System.ApplicationException类的拓展。
通常的情况下,我们编写的代码都依赖于返回有意义的错误代码的函数,通过这些代码通知函数的调用者有关的错误信息。这一策略存在的主要问题是调用函数可能会忽略检查返回的错误代码而错误地假设调用成功了。使用异常则不可能忽略函数调用过程中发生错误的事实。如果调用函数没能捕获被调函数扔出的异常则会完全绕过相应的处理,异常继续沿着调用顺序链上行直到最终遇见合适的异常处理器。
第2个问题是这样的:除非小心应付,否则返回的代码完全可能正好等于预先规定的其他错误条件。开发者们有时这样处理这个问题:设置返回代码的范围或给特定错误消息族加上前缀。异常则以其天生所具有的完整意义的对象这一优点给其特定的类型赋予了真实的意义。它们既可以像对象继承层次那样的简单也可能会复杂到需要你自己设置特定的错误条件或当作相关错误组处理。
看你怎么想了,On
Error这样的错误处理仍然存在于VB.NET之下——也许是幸事也许是场灾难。它的工作原理同VB6编程是一样的,只是彻底升级为兼容.NET的结构化异常处理系统。是的,你完全可以继续采用旧式的Err对象,因为它提供了GetException方法返回System.Exception对象,而后者正好代表了当前错误的可能信息。Err.Raise方法扔出可以被try代码块捕获的真正异常(哪怕是用其他.NET语言编写的),On
Error语句也一样。在VB.NET环境下甚至可以按照其错误号捕获异常,进一步模糊了异常处理和错误处理之间的界限。
我倾向于认为VB.NET中继续保留On Error是不适合的。实际上,我发现Vb.net的错误处理系统因为这种“精神分裂”而显得非常令人恼火。这简直就是错误处理功能的可怕大杂烩,而其原因却不过是为了实现什么该死的后向兼容!我希望这一局面不会持续太久。不管是
VB.NET还是其他.NET语言都最好不要再出现这样的情况。因此,坚持VB6错误处理风格编程的人实际上牢牢捆在了VB.NET上而丧失了.NET开发的最大优点之——在工作中使用最好、最适用的编程语言的自由。
有时,变化也许就意味着更好。