第七章 结构化异常处理
本章主要介绍了使用结构化异常处理来处理C#代码中的运行时异常。不仅阐述了处理异常的关键字,还了解了应用级和系统级异常区别和异常的基类。
7.1 .NET异常处理
.NET结构化异常处理是一项适合处理运行时异常的技术。它提供了一种标准的技术来发送和捕获运行时错误,这就是结构化异常处理(SEH)。其优点就是给开发人员有了统一的对.NET领域内各语言相同的途径来处理异常。另外,它提供了易懂的问题描述和帮助信息。
.NET异常处理有四个要素:
(1)一个表示异常详细信息的类类型:比如基类System.Exception类或者自定义的类。
(2)一个向调用者引发异常类实例的成员:也就是一个产生throw语句的地方,那么如何知道异常类中哪些成员有哪些可能的引发异常呢?可以通过SDK文档来查询一个方法,那里会列出这个方法可能引发的异常,另外,在VS.NET平台中,通过悬停鼠标于某个方法,也可以提示这个成员可能引发的异常,如下图所示,它就表示,在执行ReadLind方法时,有可能抛出三种异常。
(3)调用者的一段调用异常成员的代码块:也就是有可能出错的普通代码,如int a=int.parse(console.writeline());
(4)调用者的一段处理(或捕获)将要发生异常的代码块:try/catch块。
所有的用户定义和系统定义的异常最终都继承自system.exception基类(当然,它又继承自object)。他具有多个构造函数,可重写的方法和属性。它的属性如TatgetSite、StackTrace、HelpLink、Data在获取异常详细信息时很有用!
7.2引发异常
我们可以在某个类中的某个方法执行时,当判断出错时给出一条错误提示信息(如messagebox.show(“出错啦!”)),当然,也可以使用C#的throw关键字将错误对象返回个调用者,如:
public void accelerate(int data) { //正常的一些执行代码 if (data>100) //抛出异常
throw new exception(“出错啦!”);
//当然,也可以先实例化一个exception,设置一些属性,然后抛出 }
注意,如果我们引发一个异常,总是由我们决定所引发的问题和何时引发异常。异常应当仅仅在一个较为致命的条件满足后引发,决定什么条件下引发异常是应该应对的一个设计问题。
7.3捕获异常
因为上面已经引发了异常,那么调用者在调用这个方法时,如果处理或者捕获这个可能的异常呢,应该使用try/catch块,一旦捕获到异常对象,将能够调用捕获到的异常类的成员来释放问题的详细信息。
try() { accelerate(10); } catch(exception e) //捕获accelerate方法有可能抛出的异常 { console.writeline(e.message); } finally { }
其中try是执行过程中可能引发异常的声明的一部分。如这里应该调用accelerate()方法写在这里。
如果没有在try中触任何异常,相应catch块就被直接略过。如果try中的代码触发了异常,try的剩余代码将不被执行,程序立刻流入相应的catch块,catch块可以有多个,用于捕获不同类型的异常,是否能进入某个catch块,取决于捕获的异常是否为这个catch后面声明的异常类一致(或为它的父类)。记住,只能进入到第一个匹配的catch块,因此,应该将特定的异常放在前面,将通用的或者范围更广的放在后面哦!如果捕获到异常,但是没有相应catch块匹配,则运行时错误将中断程序,并弹出错误框哦,这将非常妨碍最终用户使用我们的程序。如下图所示:
当然,在调试时,弹出框可以让我们判断错误的详细信息,点击查看详细信息,可以查看这个异常对象的所有信息,如下图所示:
另外,也支持通用的catch语句,它不显示接收由指定成员引发的异常对象,也就是无需后面的(exception e),但这不是推荐方式,因为无法输出异常的信息,仅仅是在try捕获异常后,执行一些异常处理的普通代码。
再次引发异常
可以在catch中向之前的调用者再次引发一个异常,仅仅需要在这个块中使用throw关键字就行了,它通过调用逻辑链传递异常,这在catch块只能处理即将发生的部分错误的时候很有用:
try { } catch(CarIsDeadexception e) { //执行一些处理此错误的操作并传递异常
throw; }
当然,这里没有显式重新抛出CarIsDeadexception对象,而是使用了不带参数的throw关键字,这样可以保留原始对象的上下文。
注:无参的throw只能在catch中,它将把当前catch块所捕获的异常进行抛出,应该在这个try/catch外,再用try/catch来捕获此异常,否则将会交给CLR来捕获,这并不是一个推荐的方式哦。
内部异常
也就是说,我们完全可以在处理其他异常的时候再触发一个异常,例如,在catch中处理一个异常时候,需要读取文件,那么有可能会出现文件不存在的异常,因此,在这个catch中捕获这个异常也是可以想象的。最好的习惯(推荐,但不是强制)是将这个新异常对象标识为与第一个异常类型相同的新对象中的“内部异常”。之所以需要创建一个异常的新对象来等待处理,是因为声明一个内部异常的唯一途径就是将其作为一个构造函数参数,下例其实就是上面的一种扩展:
try { } catch(CarIsDeadexception e) { try { } catch(exception e2) { //异常处理
throw new CarIsDeadexception(e.message,e2);//引发记录新异常的异常,还有第一个异常的相关信息 } }
注意,这个新的内部异常并没有被外层的try/catch块所捕获,而是要调用者利用try/catch去捕获,这时捕获的异常应该是内部异常的类型(按上面所推荐的,它应该和外部异常类型一致),从而可以通过InnerException来访问捕获的异常的详细信息。否则将会发给CLR进行捕获,这将导致弹出错误提示框。
在一个try/catch块后面可能接着定义一个finally块,他不是必须得,是为了保证不管是否异常,一组代码语句始终都能被执行!
扩展话题,关于try/catch/finally:
7.4异常类
.NET异常分为系统级异常和应用级异常。
系统级异常:.NET基类库定义了许多派生自System.Exception的类,准确的说,这些由.NET平台引发的异常应该为系统异常,这些异常被认为是无法修复的致命错误。系统异常直接派生自System.SystemException的基类,该类派生自System.Exception。其作用就是当一个异常类派生自System.SystemException时,我们就可以判断引发异常的实体是.NET运行库而不是正在执行的应用程序代码库。仅此而已。简言之,系统级异常就是.NET平台(CLR)的各种类中事先定义好的异常,而并非用户所编写的。
应用级异常:自定义异常应该派生自System.ApplicationException。实际上应用程序级异常唯一目的就是标识出错的来源。可以判断异常是由正在执行的应用程序代码库引发(throw)的,而不是又CLR基类库或者.NET运行时引擎引发 (throw)的。
实际上上述两种异常都不在System.Exception一组构造函数外再定义其他任何成员,仅仅是为了区别是何种类型的异常。
用户当然可以一直引发System.Exception的实例来表示运行时错误,但有时候构建一个强类型异常来表示当前问题的独特细节更好!有个原则,就是何时需要构建自定义异常:仅需在出现错误的类与该错误关系紧密时才需要创建。例如,一个自定义文件类引发许多文件相关错误。
对于用户想构建的自定义异常,建议继承自System.ApplicationException,这是一个最佳实践,当然,继承自System.Exception也不会出错!而且作为一个规则,建议将自定义异常类声明为公共类型(public),因为默认是internal类型,但是由于异常类通常都跨程序集边界进行传递,所以还是公共类型好。
之后,应该去重写父类的属性和方法即可。
一个严格规范的自定义异常类,需要确保类遵循.NET异常处理的最佳实践,需要:
这样一个规范,还好VS.NET平台提供了代码片段模板,它能自动生成遵循上述最佳实践的异常类,在需要建立的地方右键-插入代码段-visual c#-exception,就可以啦!