关键词:异常,异常捕获,异常处理
0.概述
异常(Exception)是指程序在运行时可能出现的会导致程序错误运行甚至终止的错误。这种错误是不能通过编译系统检查出来的,带有一定的偶然性。同时,通过正确的程序编写规范,异常也是可以预知的,进而也是可以解决的。而异常处理机制就是针对异常的发生、通知和处理的一种机制[1]。在一些编程语言中(如C++、Java、C#等),提供了常见的异常处理关键字,有try、catch、finally、throw等,这些关键字提供了一整套异常处理的流程:有些用于在执行代码时捕获可能出现的异常,有些用于在发生异常后进行“善后处理”,有些则是在编写程序时抛出可能发生的异常。
当异常发生时,如果使用了程序提供的异常捕获机制,那么程序将自动捕获异常,并提供异常发生时程序调用的堆栈信息。如果程序的编写者没有按照要求捕获异常,那么异常就会继续向上抛出,直到被捕获为止。有些编程语言提供了异常类,用于封装不同类型的异常(如Java),有些则可以抛出任何对象(如C++)。这样看似简单的一种机制,在实际的使用中,却往往不能达到很好的效果。这是因为许多程序的编写者在使用程序提供的异常处理机制时,没有弄清楚异常的起因和解决时机,异或时为了所谓的效率优化放弃使用异常。下面针对平时使用中的一些注意事项,提出个人的看法和理解。
1.通过抛出异常通知发生错误,而非返回值
很多程序的编写者喜欢在方法执行错误时通过返回一个表示错误的值,来通知调用者。比如,在Java里这个错误值往往是空引用null,或是一个布尔变量。但是,这样的处理方法会将异常发生的原因屏蔽掉,而这又往往是我们解决问题的重要依据。
有些情况下,程序的编写者是为了快速完成程序的编写,而忽略了这个重要的步骤。其实,程序的编写并非一朝之功,前期的一点不慎有可能导致后期的重大负担,这是可以也是应该避免的。
2.在C++中也要使用异常
尽管异常处理也是C++98中早已有之的,但在很多C++程序中,编写者并不喜欢使用异常机制。有一种可能是,C++提供了非常灵活的异常处理机制,可以抛出任何类型[2],而非Java中的“可被抛出的”(Throwable)类型。这种灵活的机制让人感到无所适从,以至于很多程序员特别钟爱这种写法:
当这段代码发生错误时,我们能得到的只有一个false的返回值,这个函数只能告诉我们发生了错误,并不能告诉我们哪里错了、为什么错了、怎么解决等等。有的程序员为了解决这个含糊不清的问题,用返回int去代替返回bool,给不同的整型值赋予不同的含义,然后仅仅把这些含义写在注释里,或是自己的脑海中,给后期维护带来了巨大的困难。有人干脆把这些信息写在枚举里,且不论当不同平台、不同语言、不同环境中的程序互相调用时,是否还能识别这些枚举。想想看这个问题,当我们维护程序,发现原来的代码,可能还有一种错误没有处理时,居然还要去增加枚举的值。这背后可能会带来上层代码修改的连锁反应、代码和二进制兼容性的重大问题。真难以想象这种代码还会大量的出现在现在的业务系统中。
现在看到了吧,为了处理这个异常带来的问题,我们引入了新的问题,结果也只是越陷越深。其实解决方案很简单,定义一个自己的异常类,在发生错误的时候,抛出这个异常。在异常的成员变量中,可以包含抛出异常的位置、原因,甚至可以加上对异常的处理建议。当底层代码需要修改的时候,上层代码也可以不变,因为只要在新的错误发生的地方,抛出异常就行了。
连C++中new失败时,都是抛出一个异常,而非返回NULL,我们还有什么理由不去使用异常呢?
3.定义自己的异常
这一个议题隐含的一个说法是,指定具体的异常。
当我们自己处理自己的业务代码时,往往找不到一个现成的而又合适的异常,这时候我们可能会直接抛出基类Exception。这样又陷入了另一个问题,抛出基类异常会让问题变的含糊不清。
合适的做法是,在一个功能模块定义一个自己的异常,然后所有的功能代码都选择抛出它。定义一个异常类其实非常简单,简单的几句代码就可以:
这么做的原因是,优秀的代码自己本身就是注释,类的名字就带有分类的作用。抛出具体的异常,有利于一目了然地发现问题。好比我们一看到SQLException,就已经知道问题大概的所在。
所以,当你找不到这个具体的异常时,自己定义一个异常类。
4.什么时候解决异常
遇到异常的时候,什么时候应该解决它,什么时候应该继续抛出它?有时候,这真是一个难以决定的问题。合适的做法是,当你能够将这个问题完整呈现,而非管中窥豹的时候,解决它,否则继续抛出它。
举个例子,当我们写一个数据访问接口,它从数据库中读取数据多条记录,将这些数据组织成一个指定的格式给调用方。这个过程一般来说,可以分为读取数据库和数据转换两个步骤,这两个步骤都是有可能会发生异常的。我们在逐条记录的执行代码时,如果遇到了异常,需要打印到日志里。我们希望在日志中查看到的,不仅仅是为什么会发生异常,而且还需要看到哪一张表的哪一条记录发生了异常。而当你过早地把异常处理完之后,后者这些信息就已经消失了。
所以,在问题能够全面展现的时候,处理异常,否则你能够做的,只能是盲人摸象而已。
5.在方法的注释中声明可能抛出异常
在注释中声明自己可能会抛出的异常,这样调用者才会针对这些提示,去处理代码。否则,你还要去查看源代码,遇到不喜欢的代码风格时真的会令人发狂,更何况有时候你压根看不到源代码。
Java代码的throws关键字是强制声明的,其中带来的好处就是,让很多粗心的程序员不会遗漏掉注释自己方法中抛出的异常了。但throws只会强制抛出那些除了错误(Error)和运行时异常(RuntimeException)以外的异常,其余不强制声明[3]。这就对开发人员造成了一定的困扰。合适的做法是,除了非常常见的一些RuntimeException和Error,如空指针异常、数组越界异常等,其他的RuntimeException也写在throws语句中。并不是说要去捕获这些RuntimeException,仅仅是一种约定俗成,告诉调用者我可能会抛出这些你不应该捕获的RuntimeException,让开发者可以有效地避开陷阱。
在C++中有nothrow关键字去告诉调用者,我不会抛出异常,但却没法通知调用者,我抛出了哪些异常。所以,将这些信息写在函数的注释一种,就非常重要了。除此之外,还建议将并非自己的异常,而是代码中调用别人的方法时,自己没有去捕获处理的异常也写到注释中。即凡是方法中可能抛出的异常,都写到注释里。
6.总结
异常处理机制是程序设计和软件工程行业发展的重要成果之一,合理的应用这一手段,不仅会让程序更加健壮可读,也会让整个系统更加强健。理解异常机制,而非机械地使用它,可能会对程序更有帮助。
参考文献:
1.王晖媛,C++、Java异常处理机制的分析,计算机光盘软件与应用 2011年第八期:88-88
2.李春葆,C++语言程序设计,清华大学出版社,2008
论文作者:钱莉
论文发表刊物:《科学与技术》2019年第12期
论文发表时间:2019/11/15
标签:异常论文; 抛出论文; 程序论文; 代码论文; 自己的论文; 机制论文; 编写者论文; 《科学与技术》2019年第12期论文;