内容


改进错误处理风格

Comments

一个精致的异常系统是现代编程语言所具有的最有特色的优势之一。可是,许多经验丰富的程序员仍不知道怎样才能用好异常。或者,更准确地说,他们没有以我认为最好的方法来使用异常。其结果之一,就是使他们系统的安全性受损。因此,让我们来看看有什么可以改进的地方。

如何考虑异常

教科书和类似的参考资料一直以来都集中在异常的语法和局部语义方面。通过它们的充分介绍,大多数程序员都能阅读带有异常的代码并能解释其作用。它们所欠缺的是一种对有效风格的感觉。要找到这种感觉,您需要,

  • 重点了解您要求异常为您解决的问题,
  • 如何捕获异常,以及,
  • 如何抛出异常。

本月的专栏文章列举了几个示例来说明如何实现上述三点。

在研究这几个示例之前,不妨采用一种可能与您第一次学习异常时不同的方式来考虑异常,“热热身”。由您喜欢的语言提供的异常系统是 适合于最终用户查看的。相反,可以把异常当作 脚手架,在完成应用程序之后,再“拆除”这些“脚手架”。也许您曾在课堂上学过阅读这样的异常,如下所示

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. 不匹配的捕获
    try:
        process(some_file)
    except:
        alert("error in opening" '%s' % some_file)

发现问题没有?异常的语法和语义不匹配,这有点类似于一个公务员,在向选民承诺要注意他们最关心的问题,尤其是游泳池开放时间。尽管这样的语句形式上正确,其失衡却会使听众感到震惊,暗示有更深层的问题。

上面这个不匹配的捕获也存在类似问题:它捕获了 所有错误,但仅仅只报告了“打开文件时出现错误(error in opening)”。这样写会好一点:

清单 2. 均衡性较好的捕获
    try:
        process(some_file)
    except IOError:
        alert("error in opening" '%s' % some_file)

许多程序员由衷地认为这两个例子是等价的,因为一种可能的理由是,“记录 process 只是用来生成 IOError ”。在这层意义上讲,应用程序在这两个示例中的执行,的确毫无差别。可是源代码不只是给计算机用的;更重要的是必须向身为人类的读者表达其含意。如果您的代码假设某个特定的异常一定是一个 IOError ,那么利用该语言的精确性, 就这么说

第二个示例仍不能完全防止让最终用户看到“原始”异常的危险。实际上,即使 process()在当前版本中被明白无误地记录为仅抛出 IOError ,但我仍要求在编写该段代码时至少达到下面这样的详细程度:

清单 3. 形式均衡且全面的捕获
    try:
        process(some_file)
    except IOError:
        alert("error in opening" '%s' % some_file)
    except:
        alert("internal and completely unexpected problem")

当然,对我们而言,拥有完整且正确记录的接口是一种少有的奢侈。在开发工作中许多语言采用了一种有用的技术 - 使用异常系统内置的内省。这使我们的示例变成这样:

清单 4. 形式均衡且全面的捕获,并带有信息性的“缺省设置”
    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))

举例来说,如果 process() 的实现产生了一个导致 ValueError 而不是 IOError 的错误,上面最后一个处理程序至少会将 ValueError 作为类名报告上来。

在捕获时还常有另一种含糊不清之处。其代码像这样:

错误处理中过宽的作用域
    try:
        first_operation()
        second_operation()
        third_operation()
        fourth_operation()
    except:
        alert("something went wrong")

这里的不足之处在于“横向”的不精确;当出现 something went wrong 时,没有能立即与引发错误的特定 *_operation() 连接。一种简单的解决方案是一次只捕获一段,这样前面的编码就变成:

清单 6. 错误处理中更高的精确度
        # 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()

稍微复杂一点的解决方案是让捕获代码的范围更宽一些,但要使用语言的内省能力来报告追溯信息:

清单 7. 许多语言能管理自己的追溯
    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)

于是,在捕获异常时,确信获取了全部异常且其处理方式是精确的,并使用您所选择的语言中的可用信息合成出有用的输出。

抛出

同异常的使用相比,生成异常是一个稍许高级的主题。但是,所有 服务器诊所的读者都应知道抛出异常的基础:

  • 记录接口。
  • 保持简单的继承层次结构。

继承是由语言支持的用于异常值的出色技术;按 IOErrorValueErrorAppError 等类别组织错误是一种相当有用的方法。然而,没有经验的设计人员常将他们的继承层次结构复杂化,以至于同最优的层次结构相差甚大。如果您发现在异常类中定义了两层以上的继承级别,那么就要检查一下。如果有三层以上,或者在一个异常超类中子类的个数超过了七个,那我打赌一定有什么地方出错了。

在这方面经常犯的一个错误是在 Exception 或者 Error 下复制了一棵应用程序对象树。这几乎总是是个错误,但是可以通过将异常 参数化而不是子类化来简单地修正这个错误。一个指定了 MercuryExceptionVenusException 等的设计可能不是最好的;而用 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)论坛讨论异常管理的技术细节。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 查阅 服务器诊所(Server clinic)以前的专栏文章
  • developerWorks经常从总体上介绍 Python,而其中的 服务器诊所专栏更经常对其进行专门的介绍。对于还不熟悉该语言的读者来说,2003 年 3 月刚刚由 O'Reilly 出版的 Python in a Nutshell是一份相当重要的参考资料。该书简明扼要地介绍了 Python 语言,同时也是该语言最新版本的权威性印刷参考资料。虽然从整体上讲该书是简洁甚至可以说是精炼的,但在 所有关于语言的书中,就其介绍的深度和有关良好风格的明智的建议方面,该书对异常的处理是独一无二的。
  • 虽然关于 错误和异常(Errors and Exceptions)的标准 Phyton 文档没有 Nutshell那样“雄心勃勃”,但也编写得很清晰。
  • 尽管 How to Think Like a Computer Scientist: Learning with Python很好地完成了其标题的许诺,但在异常方面则乏善可陈。 关于异常的章节只是照搬了太多程序员都知道的一些基本知识。
  • 可以得到的出版物很少关心异常系统,这种情况确实很明显。这些出版物的注意力总是放在对安全性、需求及质量的研究上。可是, Handbook of Walkthroughs, Inspections, and Technical Reviews则不同。对于它所强调的编码是“解决问题的最后手段”,以及它希望熟悉错误及其后果的积极意愿,我深表赞同。
  • 与此形成对比的是,有很多文章介绍了契约式设计及其在各种语言下的实际用法。其中最主要的一篇文章是 Building bug-free O-O software: An introduction to Design by Contract
  • EJB 异常处理的最佳做法developerWorks,2002 年 5 月)讨论了如何处理 EJB 异常。它已超出了基础范围,用于为用户解决实际需求。
  • EJB 最佳实践:构建更好的异常处理框架说明了在通过分层来分割异常处理的环境下的异常框架( developerWorks,2003 年 1 月)。
  • 掌握 Linux 调试技术developerWorks,2002 年 8 月)概述了一些工具和策略,它们有助于您更好地在第一时间跟踪程序错误。
  • The Cranky User: "I can't use thisdeveloperWorks,2002 年 5 月)研究了为何改进实用性往往是预防错误的最佳形式。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=20695
ArticleTitle=改进错误处理风格
publish-date=08032003