许多开发人员对待异常系统就好像在运用一种浪漫语言一样:熟悉其中的词语,但却不清楚要以此说明什么。Cameron 对此提出了实用的建议,指出系统应该抛出什么和捕获什么。
一个精致的异常系统是现代编程语言所具有的最有特色的优势之一。可是,许多经验丰富的程序员仍不知道怎样才能用好异常。或者,更准确地说,他们没有以我认为最好的方法来使用异常。其结果之一,就是使他们系统的安全性受损。因此,让我们来看看有什么可以改进的地方。
如何考虑异常
教科书和类似的参考资料一直以来都集中在异常的语法和局部语义方面。通过它们的充分介绍,大多数程序员都能阅读带有异常的代码并能解释其作用。它们所欠缺的是一种对有效风格的感觉。要找到这种感觉,您需要,
* 重点了解您要求异常为您解决的问题,
* 如何捕获异常,以及,
* 如何抛出异常。
本月的专栏文章列举了几个示例来说明如何实现上述三点。
在研究这几个示例之前,不妨采用一种可能与您第一次学习异常时不同的方式来考虑异常,“热热身”。由您喜欢的语言提供的异常系统是不适合于最终用户查看的。相反,可以把异常当作脚手架,在完成应用程序之后,再“拆除”这些“脚手架”。也许您曾在课堂上学过阅读这样的异常,如下所示
caught exception in main()
java.lang.SomeException: ugly input
at ...
当然,这个技巧对程序员是有价值的。可是它绝不能被强加给最终用户。一个完整的应用程序应该从来不说“有异常”;所有呈现给最终用户的报告都应该用下面的这种本机语言来写,或许更接近于这样
The configuration file 'folder/thing.cfg'
appears to be corrupt, as line #17 cannot
be parsed.
正是这种清晰性对应用程序的安全性施加了直接的压力。其原因在于:用户及其管理员一次又一次地证明他们对不能理解的事物的反应,是简化系统直至得到自己期望的行为。如果他们读到“未发现文件(file not found)”,他们会随意地从别处复制一些文件,而不考虑特权或许可权。而保证应用程序安全性的最可靠的方法之一就是使应用程序工作,这样用户才会理解它的运作。聪明的用户会因为急于“让程序工作”而破坏几乎所有安全性设置。
不是所有程序员都认同我这种观点。有不少高级软件工程师冷静地提议说,用户输入错误的数据或错用应用程序都是咎由自取。我在这里不是要讨论这种态度的道德问题;只是注意到,在开发人员和最终用户采取这种互相对立的姿态时,安全性正在不断地被破坏。
因此,在某个特定开发项目中,使用异常的第一步,也往往是最容易被忽视的一步,就是确定程序对异常的需求。这一点一定要搞清楚。当客户或主管在指示程序应如何处理格式良好的输入数据时,抓住机会,对万一发生错误时程序的具体操作细节同他们达成共识。给自己足够的时间去会见客户。想象一下:最终客户可能会把同程序“接触时间”的大部分都消耗在查看程序所显示的错误消息上。这并不是骇人听闻,对许多应用程序来说“正常”操作是相当快的,而对于错误的响应,人们需要花不少时间来思考。错误消息及对应操作同程序的其它部分相比,值得花同样多的技术。
事实上,我会对那些让最好的人才集中精力于错误处理方面,而不是传统编程中比较“花哨”的方面(比如图形用户界面(GUI)外观编程)的项目,更感到高兴。理由在于:一个有错误但带有优秀的错误处理机制的应用程序,比近乎完美但其错误处理机制却不友好的应用程序,更能赢得最终用户的欢心。
在完成了第一轮需求分析后,您手中拥有的这些叙述性说明会使程序的异常处理设计更为合理且更有价值。现在的挑战则是这篇专栏文章的读者所感兴趣的技术问题。
捕获
Python 作为一种方便的工具,可用来表达示例用法。我经常遇到类似于这样的缺陷:
清单 1. 不匹配的捕获
[myphp]
try:
process(some_file)
except:
alert("error in opening" '%s' % some_file)
[/myphp]
发现问题没有?异常的语法和语义不匹配,这有点类似于一个公务员,在向选民承诺要注意他们最关心的问题,尤其是游泳池开放时间。尽管这样的语句形式上正确,其失衡却会使听众感到震惊,暗示有更深层的问题。
上面这个不匹配的捕获也存在类似问题:它捕获了所有错误,但仅仅只报告了“打开文件时出现错误(error in opening)”。这样写会好一点:
清单 2. 均衡性较好的捕获
[myphp]
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
[/myphp]
许多程序员由衷地认为这两个例子是等价的,因为一种可能的理由是,“记录 process 只是用来生成 IOError”。在这层意义上讲,应用程序在这两个示例中的执行,的确毫无差别。可是源代码不只是给计算机用的;更重要的是必须向身为人类的读者表达其含意。如果您的代码假设某个特定的异常一定是一个 IOError,那么利用该语言的精确性,就这么说。
第二个示例仍不能完全防止让最终用户看到“原始”异常的危险。实际上,即使 process() 在当前版本中被明白无误地记录为仅抛出 IOError,但我仍要求在编写该段代码时至少达到下面这样的详细程度:
清单 3. 形式均衡且全面的捕获
[myphp]
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
except:
alert("internal and completely unexpected problem")
[/myphp]
当然,对我们而言,拥有完整且正确记录的接口是一种少有的奢侈。在开发工作中许多语言采用了一种有用的技术 - 使用异常系统内置的内省。这使我们的示例变成这样:
清单 4. 形式均衡且全面的捕获,并带有信息性的“缺省设置”
[myphp]
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
except:
(exc_class, exc_object, exc_traceback) = sys.exc_info()
alert("""internal and completely unexpected problem,
manifested as %s""" % str(exc_class))
[/myphp]
举例来说,如果 process() 的实现产生了一个导致 ValueError 而不是 IOError 的错误,上面最后一个处理程序至少会将 ValueError 作为类名报告上来。
在捕获时还常有另一种含糊不清之处。其代码像这样
错误处理中过宽的作用域
[myphp]
try:
first_operation()
second_operation()
third_operation()
fourth_operation()
except:
alert("something went wrong")
[/myphp]
这里的不足之处在于“横向”的不精确;当出现 something went wrong 时,没有能立即与引发错误的特定 *_operation() 连接。一种简单的解决方案是一次只捕获一段,这样前面的编码就变成:
清单 6. 错误处理中更高的精确度
[myphp]
# The documentation assures us these
# two can't toss exceptions.
first_operation()
second_operation()
try:
third_operation()
except:
alert("something went wrong in 'third_operation()'")
# This, also, cannot throw an exception.
fourth_operation()
[/myphp]
稍微复杂一点的解决方案是让捕获代码的范围更宽一些,但要使用语言的内省能力来报告追溯信息:
清单 7. 许多语言能管理自己的追溯
[myphp]
try:
first_operation()
second_operation()
third_operation()
fourth_operation()
except:
exc_traceback = sys.exc_info()[2]
stack_list = []
while 1:
stack_list.append(exc_traceback.tb_frame.f_code.co_name)
if not exc_traceback.tb_next:
break
exc_traceback = exc_traceback.tb_next
# The next is an almost-human-readable
# description of where the fault occurred
alert("something went wrong in %s" % stack_list)
[/myphp]
抛出
同异常的使用相比,生成异常是一个稍许高级的主题。但是,所有服务器诊所的读者都应知道抛出异常的基础:
* 记录接口。
* 保持简单的继承层次结构。
继承是由语言支持的用于异常值的出色技术;按 IOError、ValueError、AppError 等类别组织错误是一种相当有用的方法。然而,没有经验的设计人员常将他们的继承层次结构复杂化,以至于同最优的层次结构相差甚大。如果您发现在异常类中定义了两层以上的继承级别,那么就要检查一下。如果有三层以上,或者在一个异常超类中子类的个数超过了七个,那我打赌一定有什么地方出错了。
在这方面经常犯的一个错误是在 Exception 或者 Error 下复制了一棵应用程序对象树。这几乎总是是个错误,但是可以通过将异常参数化而不是子类化来简单地修正这个错误。一个指定了 MercuryException 和 VenusException 等的设计可能不是最好的;而用 PlanetException 进行编码,并附有数据指出哪一个 planet 是问题所在的设计可能会更好。
有一种稍许微妙的风格,可支持契约式设计(design-by-contact,DbC)方法,即在 AssertionError 之外放弃对任何异常类的定义,然后只用断言来编码所有异常接口。
高级异常
对于异常还有很多可学的,其中只有很小一部分由印刷出版的书籍详细充分地介绍过;保护异常使它们不被用来破坏安全性;异常度量;异常设计的性能后果;调试、基准测试及验证异常的方法;异常和资源管理;以及更多其它内容。在本月,最后要提到的一点是,用多语言编码的异常系统的重要性。
服务器诊所经常提倡“双级”编程 - 组合两种不同语言 -来利用各自的长处。如今这种方法已经十分流行和“普通”,有不少系统采用了这种方法,其中包括在两种语言之间进行对象“转换”。David Beazley 的简化包装器和接口生成器(Simplified Wrapper and Interface Generator,SWIG)就是用于这类工作的工具,并得到了极为广泛地应用。
虽然之前有所忽视,但教会不同语言的异常系统相互之间如何进行有效的交流一直都是需要的。这正是芝加哥大学计算机科学系助理教授 Beazley 如今正面对的一项挑战。他的包装应用程序生成器(Wrapped Application Generator,WAD)“是一个用于简化调试脚本编制语言扩展工作的嵌入式调试系统”。通过下面列出的参考资料可以学到更多 WAD、SWIG 以及其它异常方面的高级话题。同时也请记住访问 Linux 专区的脚本编制诊所(Scripting clinic)论坛讨论异常管理的技术细节。