“一般地,在程序代码中犯错比不犯错更容易。”
不管看起来是否违反直觉,我觉得存在一种半正式的论据。在同样心态下,也有一些有趣的推论。
首先,为什么改错比犯错更难,尤其在软件工程中,更是如此?
让我们尽量从熵、混沌注1和有序的视角来分析。
下图是有序:
下图是混沌:
我们大概都了解混沌和有序,通常不必费力思考原因的一个现象就是,我们更容易给系统创造混沌而非有序。一条狗很快就能把你的卧室搞乱,而整理好房间 却需要视角和某种精神上的努力。再举个更为尖刻的例子:一家建筑公司倾其全力造好一栋建筑需要数年时间,而把该栋建筑破坏掉,只需要一个人,只要他足够多的炸药,瞬间就能完成。
创造比破坏难,原因在于系统内部所具有的混沌状态,要远远大于有序状态。当然,这取决于有序的定义和界定,但是,如果和无序状态比起来,任何合理的 定义都会让你得到更少的、可能有序的状态。考虑一下卧室里的袜子:为了创造混沌,你只需肆意甩出去,不必多想。狗狗就能做到。但是,为了将来需要时能快速 找到,整理这一双双袜子、并按照某种方式摆放好,很明显就需要更多努力,还有某种认知和思考。
因此,问题就变成了可能状态(混沌)的数量,和相应使其有意义的一个子集(有序)。
那么,程序代码中的错误是怎么回事?bug 究竟是怎样产生的?
当你的代码未能按照预期执行时,就说明存在 bug 了。(让我们把可计算性的话题放到另一篇博文,因为它比较复杂。某个任务在合理时间内,可能有解决方案,也可能没有,不过,我的意图是,存在可做的先验。我们简化一下问题:程序未能按照意图运行。)
比如,你需要一个函数,它返回两个 int 值的平均数(整数)。首先,你可能这样写代码:
int avg (int a, int b)
{
return (a + b) / 2;
}
正确吗?
错。它没有考虑可能存在的整数溢出,而两个整数的平均数应该总是介于 a 和 b 之间。换句话说,avg () 函数被期望总是返回一个(四舍五入)的结果,且不会溢出。
让我们修复一下。一个新手程序员可能会按照如下方式「修复」:
int avg (int a, int b)
{
return a / 2 + b / 2;
}
当然,又错了。
在某个互联网论坛上,当被问到根据其最初形式修复 avg () 时,总是会给出建议,要求把参数的数据类型转换为浮点,再把结果四舍五入为 int 型:再说一次,糟糕想法会产生奇怪的结果,即使有最好的结果,也一定没有效率。两个 int 值的平均数计算竟然用到了浮点,你是认真的吗?
我们再考虑一种更好的方式。这一次,显然修复成了:
int avg (int a, int b)
{
return a + (b - a) / 2;
}
正确吗?
不正确!尝试 avg (INT_MIN, INT_MAX) 的情况,你就明白错在哪里了。事实上,如果 a 或者 b是负数,(b-a) 甚至在达到接近边界值 INT_MIN 和 INT_MAX 之前就相对容易溢出了。
最后的解决方案呢?如果我没有遗漏什么的话,我相信下面的代码就是最好的了:
int avg (int a, int b)
{
if ((a < 0) != (b < 0))
return (a + b) / 2;
else
return a + (b - a) / 2;
}
首先,上面代码中有趣的地方较为明显了,软件 bug 常常是没有考虑到、漠视或忘记了某些东西,也有可能是压根儿不知道。
在每行源代码里,存在着一定数量的对象,这些对象又包含了特定属性。在 avg () 例子中,对象有:a, b, 2, +, /, result。在 bug 让你损失数十亿美元的云服务或太空船之前,需要定位并修复它,你应该查看表达式/语句所涉及到的所有对象,并思考你掌握它们的所有知识。比如,a 和 b 是 int 型,因此它们必定总是在某个限制内(现实往往比较残酷);+ 操作符在大多数语言中因为没有警告溢出而臭名昭著;除法容易搞砸 int 型,也禁止被零除。幸亏我们多多少少习惯了。
在现实生活中,它太简单了,以致于我们忘记、漠视或对上面提到的知识点一无所知。对于既定的一项任务,只存在很少的、形式上正确的方案,通常,有一 种方案最简短,但是还有很大可能隐藏着 bug。如果你在写代码时,脑子里需要记住 10 个因素,只要你无视了其中一个,就会导致 bug 的产生。我们的大脑还不够完美:它倾向于忘掉那些不应该忘掉的东西。
我们对找到 avg () 的最佳解决方案的探求,应该已经提醒你注意卧室里、那条淘气的狗狗了。犯错很容易,而找到正确的方案,相对难一些。
为了采取相对正式的方法,假定你被安排了一项先验的计算任务,那么你需要决定为编码投入多少努力。有两个极端:零努力、无穷多的努力。
对于零努力的情形,如果你无论如何都要做点儿事情的话,那就是在机器上扔一个随机的位元流注2,观察运行情况。这种做法属于十足的混沌:用随机序列 来解决某个特定问题的可能性微乎其微,就好像把一堆球扔到台球桌、还期望它们能够组成三角形。各种可能性太多,导致正确的可能性几乎不会发生。
对于无穷多努力的情形,为了找到最便捷、最牛逼的解决方案而投入了足够多的努力,当然你会找到的。这是有序,很难。
不是很难,而总是更难。