第3条:确保前置条件与后置条件都能够得到满足
修理电子设备的时候,我们首先要检查供电是否正常,也就是检查电流有没有从电源模块正确地流入该设备的电路中。在很多情况下,这项检查都能帮助我们找出问题。计算机程序与之类似,很多问题也可以通过对例程的入口点(entry point)与出口(exit)进行检查而得以确定。入口点就是前置条件(precondition),它指的是程序在即将执行例程时所具备的状态,以及传递给该例程的输入值,出口则是后置条件(postcondition),它指的是程序执行完例程之后的状态及其返回值。如果前置条件得不到满足,那说明用来设置这些前置条件的代码里面有错误,若是后置条件得不到满足,则说明该例程本身有问题。如果两者都正确,那么应该转向其他地方去寻找bug。
我们可以在例程开始的地方、调用例程的地方或关键算法开始执行的地方设置断点(参见第30条)。为了判断前置条件是否得到满足,我们应该仔细检查算法的参数,包括传入的参数值,调用方法时所针对的对象,以及可疑代码所使用的全局状态。尤其要注意以下几点:
- 找出那些本来不应为null,但实际上却为null的值。
- 调用数学函数的时候,确保传入的值位于该函数的定义域之内,例如,调用log函数时传入的值应该大于0。
- 查看对象、结构体与数组的内部细节,确保其内容符合要求。这也可以帮你查出无效的指针。
- 检查变量的取值是否在合理范围之内。如果变量具有6.89851e-308或61007410这样的可疑取值,那通常表明它还没有初始化。
- 检查传给例程的数据结构是否正确,例如,map有没有包含预期的键与值,双向链表(doubly linked list)能不能正确地遍历。
然后,我们应该在例程结束的地方、调用完例程的地方或关键算法执行完毕的地方设置断点,以判断该例程的执行效果是否正确:
- 计算出来的结果看上去合理吗?有没有处在预期的范围之内?
- 如果结果合理,而且位于预期的范围之内,那么实际的值是否正确?我们可以通过手算来演练相应的代码,以验证计算机的执行结果是否正确(参见第38条),也可以将执行结果与已知的正确值相对比,或是采用其他工具或方法来进行验算。
- 例程的副作用是否符合预期?可疑代码所接触到的其他数据是否遭到破坏或拥有了不正确的取值?有些算法在遍历数据结构时,会把一些维护其工作所用的信息记录在数据结构中,对于这些算法来说,尤其应该进行这样的检查。
- 算法所获得的资源,如文件句柄及锁,有没有正确地释放?
同样的方法也可以用在更为高层的操作与配置环境中。例如,如果要验证SQL语句是否正确地构建了某张表格,我们可以查看它所扫描的那些表格及视图,并且看看它构建出来的那张表格是什么样子。如果要判断基于文件的处理流程是否正确,我们可以检查其输入文件与输出文件。如果要调试某个构建在Web服务上面的操作,我们可以检查其中每项Web服务的输入与输出。如果要排解整个数据中心的故障,我们可以检查其中每个元素所需要的及所提供的机制是否正确,其中包括网络连接、DNS、共享存储、数据库以及中间件等。在这些情况下,我们都必须亲自验证(verify),而不能想当然地接受假设(assume)。
要点
- 仔细检查例程的前置条件与后置条件。