Why Numbering Should Start At Zero

前言

在我们平时接触使用的大多数编程语言中,编号都是从 0 开始的。接受了这是一个惯例后,很少有人会去想为什么是从 0 开始?如果这是惯例,为什么不是其他的惯例呢?Edsger W. Dijkstra 教授的这个小备忘录也许可以解开我们的疑惑。

原文链接:http://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html

译文

为了不使用有害的三点(…)表示一个自然数的子序列 2, 3, …, 12,我们有四个惯例:

a) 2 ≤ i < 13

b) 1 < i ≤ 12

c) 2 ≤ i ≤ 12

d) 1 ≤ i ≤ 13

是否有理由让我们更喜欢其中的一个惯例呢?Yes。观察可得 a) 和 b) 有一个优点:上下边界的差等于子序列的长度。也就是说,在这两个惯例中,两个相邻的子序列意味着其中一个子序列的上界等于另一个子序列的下界。尽管这些观察是有效的,他们并不能让我们在 a) 和 b ) 之间做出选择。所以,让我们重头再来吧。

这里有一个最小自然数的问题。如果像 b) 和 d) 中那样不包含下界,将使得一个从最小自然数开始的子序列的下界进入了非自然数的范围。That is ugly。因此,对于下界我们更喜欢 a) 和 c) 中的 ≤ 。现在来考虑一个从最小自然数开始的子序列:当这个序列收缩到空序列时,包含上界会使得上界显得不自然。Ugly。所以对于上界我们更喜欢 a) 和 c) 中的 < 。综上所述,惯例 a) 是首选。

Remark Xerox PARC 开发的编程语言 Mesa 对于四种惯例中的整数间隔都有特殊符号。Mesa 丰富的经验表明使用其他三种约定一直是笨拙和错误的根源。由于这些经验 Mesa 程序员现在强烈建议不要使用后三个惯例。之所以提到这个实验证据的原因是,一些人对尚未在实践中得到确证的结论感到不安。(End of Remark.)


在处理一个长度为 N 的序列的时候,我们希望通过下标来辨别其中的元素。下一个令人烦恼的问题是要分配给其起始元素的下标值。使用 a) 的范围表示,当开始下标为 1 的时候,下标的范围是 1 ≤ i < N+1。然而,从 0 开始的话,可以得到更好的范围 0 ≤ i < N。所以,让我们的序数从零开始:在一个序列中,一个元素的序数(下标)等于在它之前的元素的数量。而且这也遵循了几个世纪以来,零作为最自然的数字的传统。

Remark 许多编程语言的设计都没有对此细节给予足够的重视。在 FORTRAN 中下标总是从 1 开始;ALGOL 60 和 PASCAL 采用了惯例 c);最近的 SASL已经回归 FORTRAN 惯例:SASL 中的序列同时时正整数的函数。Pity!(End of Remark.)


以上的论点是由最近的一个插曲而引出来的。当时,我的一位大学数学系同事(不是计算机科学家),在一次情绪爆发中指责一些年轻的计算机科学家 ”卖弄学术“。因为他们根据习惯而从零开始编号。他认为是故意采用这个(最明智的)惯例作为一种挑衅。(当然,“以 … 结尾”的惯例也被视为挑衅。但这个约定是有用的:我知道一个学生因为默认情况认为问题会在第一页底部结束而差点没有通过考试。)Antony Jay 说的对:“和其他宗教团体一样,异端者必须被清除。不是因为他们很可能错了,而是因为他们可能是对的。“

写在后面

以 Python 为例。这种思想在 Python 风格中体现为,在切片和区间操作里不包含区间范围的最后一个元素。这样做带来的好处如下:

  • 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[ : 3] 都返回 3 个元素。

  • 当起止位置信息都可见时,我们可以快速算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。

  • 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[ : x] 和 my_list[x: ] 就可以了,如下所示:

    1
    2
    3
    4
    5
    >>> l = [10, 20, 30, 40, 50, 60]
    >>> l[:2]
    [10, 20]
    >>> l[2:]
    [30, 40, 50, 60]