Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

契约、错误与异常

复习

  • 分解、模块与接口:模块通过接口交流
  • 异常处理:错误跨调用传播
  • 类型系统:约束合法操作

TL;DR

  • 每个函数或模块,都有一份“契约”:前提与承诺
  • 输入不符合预期时,应当明确地失败
  • 错误有不同类型:调用方错误、可恢复、不可恢复
  • 明确失败,远好过悄悄出错

正文

  模块之间通过接口交流。可接口上说的,不只是“函数叫什么、参数是什么类型”,还有一份更深层的约定——契约(contract)。

契约:前提与承诺

  一份契约,通常包含:

  • 前置条件(前提):调用方必须满足什么(比如“这个参数不能为负”)
  • 后置条件(承诺):只要前提满足,被调方保证做到什么
  • 不变式:在整个过程中始终成立的性质

  可以把函数看成一份“合同”:你按前提调用我,我按承诺给你结果。 双方各守其责,合作才顺利。

前提不满足时怎么办

  现实里,调用方未必守约——传了非法参数、给了坏数据。此时,被调方不能装作没事,而应明确地失败

  具体怎么做,取决于错误的性质:

  • 调用方的错误:比如参数非法。通常应当明确报错(抛出异常或返回错误),让问题立刻暴露
  • 可恢复的错误:比如文件暂时打不开。可以返回错误,让调用方决定重试或走别的路
  • 不可恢复的错误:比如内存耗尽。可能只能终止,但要尽量留下信息

  关键原则是:错误要被明确地处理,而不是被悄悄吞掉。

为什么“明确失败”更重要

  有两类糟糕的做法,都和“不明确”有关:

  • 悄悄出错:出了问题却装作正常,返回一个看似合理其实是错的结果。调用方毫不知情,错误会一路扩散,最后在一个离得很远的地方爆发,极难排查
  • 吞掉错误:捕获了异常却什么都不做。问题被“藏”起来,下次再犯还是查不出原因

  让错误尽早、明确地暴露,比让它悄悄蔓延安全得多。 这也呼应了异常处理的思路:错误的代价,往往不在于它发生了,而在于它被掩盖了。

思考题 1

  一个函数的“契约”,通常包括什么?

思考题 2

  为什么“悄悄出错”,比“明确失败”更危险?

小结

知识点

  • 契约含前置条件、后置条件与不变式
  • 输入不符合预期时应当明确失败
  • 错误可分为调用方错误、可恢复与不可恢复
  • 明确失败优于悄悄出错或吞掉错误

参考资料

  1. Wikipedia(zh):契约式设计:以契约描述模块的责任
  2. Wikipedia(zh):异常处理:明确处理错误的方式

思考题答案(仅供参考)

思考题 1

  通常包括前置条件(调用方必须满足的约束)、后置条件(满足前提后被调方保证的结果),有的还包含不变式(过程中始终成立的性质)。它们共同约定双方的责任。

思考题 2

  因为悄悄出错会让调用方以为一切正常,错误随之向远处扩散,最终在一个不相干的地方爆发,极难定位;而明确失败能让问题在发生处立刻暴露,便于及时发现和处理。被掩盖的错误,代价往往更大。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪