23 断言式编程
在自责中有一种满足感。当我们责备自己时,会觉得再没人有权责备我们。
——奥斯卡•王尔德:《多里安•格雷的画像》
每一个程序员似乎都必须在其职业生涯的早期记住一段曼特罗(mantra)。它是计算技术的基本原则,是我们学着应用于需求、设计、代码、注释——也就是我们所做的每一件事情——的核心信仰。那就是:
这决不会发生……
“这些代码不会被用上30年,所以用两位数字表示日期没问题。”“这个应用决不会在国外使用,那么为什么要使其国际化?”“count不可能为负。”“这个printf不可能失败。”
我们不要这样自我欺骗,特别是在编码时。
|
提示33 |
|
|
If It Can’t Happen, Use Assertions to Ensure That It Won’t |
|
无论何时你发现自己在思考“但那当然不可能发生”,增加代码检查它。最容易的办法是使用断言。在大多数C和C++实现中,你都能找到某种形式的检查布尔条件的assert或_assert宏。这些宏是无价的财富。如果传入你的过程的指针决不应该是NULL,那么就检查它:
void writeString(char *string) {
assert(string != NULL);
...
对于算法的操作,断言也是有用的检查。也许你编写了一个聪明的排序算法。检查它是否能工作:
for (int i = 0; i < num_entries-1; i++) {
assert(sorted[i] <= sorted[i+1]);
}
当然,传给断言的条件不应该有副作用(参见124页的方框)。还要记住断言可能会在编译时被关闭——决不要把必须执行的代码放在assert中。
不要用断言代替真正的错误处理。断言检查的是决不应该发生的事情:你不会想编写这样的代码:
printf("Enter 'Y' or 'N': ");
ch = getchar();
assert((ch == 'Y') || (ch == 'N')); /* bad idea! */
而且,提供给你的assert宏会在断言失败时调用exit,并不意味着你编写的版本就应该这么做。如果你需要释放资源,就让断言失败生成异常、longjump到某个退出点、或是调用错误处理器。要确保你在终止前的几毫秒内执行的代码不依赖最初触发断言失败的信息。
让断言开着
有一个由编写编译器和语言环境的人传播的、关于断言的常见误解。就是像这样的说法:
断言给代码增加了一些开销。因为它们检查的是决不应该发生的事情,所以只会由代码中的bug触发。一旦代码经过了测试并发布出去,它们就不再需要存在,应该被关闭,以使代码运行得更快。断言是一种调试设施。
这里有两个明显错误的假定。首先,他们假定测试能找到所有的bug。现实的情况是,对于任何复杂的程序,你甚至不大可能测试你的代码执行路径的排列数的极小一部分(参见“无情的测试”,245页)。其次,乐观主义者们忘记了你的程序运行在一个危险的世界上。在测试过程中,老鼠可能不会噬咬通信电缆、某个玩游戏的人不会耗尽内存、日志文件不会塞满硬盘。这些事情可能会在你的程序运行在实际工作环境中时发生。你的第一条防线是检查任何可能的错误,第二条防线是使用断言设法检测你疏漏的错误。
在你把程序交付使用时关闭断言就像是因为你曾经成功过,就不用保护网去走钢丝。那样做有极大的价值,但却难以获得人身保险。
即使你确实有性能问题,也只关闭那些真的有很大影响的断言。上面的排序例子
|
断言与副作用 如果我们增加的错误检测代码实际上却制造了新的错误,那是一件让人尴尬的事情。如果对条件的计算有副作用,这样的事情可能会在使用断言时发生。例如,在Java中,像下面这样编写代码,不是个好主意: while (iter.hasmoreElements () { Test.ASSERT(iter.nextElements() != null); object obj = iter.nextElement(); // .... } ASSERT中的.nextElement()调用有副作用:它会让迭代器越过正在读取的元素,这样循环就会只处理集合中的一半元素。这样编写代码会更好: while (iter.hasmoreElements()) { object obj = iter.nextElement(); Test.ASSERT(obj != null); //.... } 这个问题是一种“海森堡虫子”(Heisenbug)——调试改变了被调试系统的行为(参见[URL 52])。 |
也许是你的应用的关键部分,也许需要很快才行。增加检查意味着又一次通过数据,这可能让人不能接受。让那个检查成为可选的,但让其余的留下来。
相关部分:
l 调试,90页
l 按合约设计,109页
l 怎样配平资源,129页
l 靠巧合编程,172页
练习
19. 一次快速的真实性检查。下面这些“不可能”的事情中,那些可能发生? (解答在290页)
1. 一个月少于28天
2. stat(“.”, &sb) == -1 (也就是,无法访问当前目录)
3. 在C++里:a = 2; b = 3; if (a + b != 5) exit(1);
4. 内角和不等于180°的三角形。
5. 没有60秒的一分钟
6. 在Java中:(a + 1) <= a
20. 为Java开发一个简单的断言检查类。 (解答在291页)







