这里提出的一个问题使我想起了我与一位程序员的讨论。他认为,从零开始的数组应该替换为从一开始的数组,因为从零开始的数组是一个实现细节,它起源于数组和指针以及计算机硬件的工作方式,但是这些东西不应在更高层次上反映出来。语言。
现在我不是很擅长辩论,所以除了提供感觉更合适之外,我无法提供任何充分的理由坚持使用从零开始的数组。为什么是零的共同出发点阵列?
这里提出的一个问题使我想起了我与一位程序员的讨论。他认为,从零开始的数组应该替换为从一开始的数组,因为从零开始的数组是一个实现细节,它起源于数组和指针以及计算机硬件的工作方式,但是这些东西不应在更高层次上反映出来。语言。
现在我不是很擅长辩论,所以除了提供感觉更合适之外,我无法提供任何充分的理由坚持使用从零开始的数组。为什么是零的共同出发点阵列?
Answers:
我认为我们谁都不能提供比Edsger W. Dijkstra的文章“为什么编号应从零开始”更强有力的论据。
好吧……显然,大多数语言,包括最近的语言,都是从零开始的。由于这些语言是由相当熟练的人编写的,因此您的朋友一定是错的。
为什么1会比零更好的起始索引?为什么不2或10?答案本身很有趣,因为它显示了人们捍卫这一想法的过程。
在第一个说法是,它更自然,因为第一次通常是一个所有在别人面前,至少对于大多数人...
第一个参数是最后一个索引也是数组的大小...
我通常对这种争论所听到的原因的“品质”给我留下深刻的印象……当我被提醒时,更是如此……
……“基于基础的”表示法是西方文化遗留下来的遗物,即使有更多的世纪,它也忽略了零的存在。
信不信由你,原始的公历从-3,-2,-1,1,2,3 ...想象一下它对西方科学造成的问题(例如,从1月1日开始-2年到1月2日,就可以看到原始的公历与减法之类的简单冲突……)。
保持基于一的数组就像(嗯,我会为此而被下调... ^ _ ^ ...),保持到21世纪的英里和码。
首先(糟糕,抱歉,我再试一次)
零,零什么都不是,一是什么。一些宗教文献认为,“一开始什么都没有。” 一些与计算机有关的讨论可能像宗教辩论一样炙手可热,因此,这一点似乎并没有超出主题范围……^ _ ^
首先,使用基于零的数组并忽略其第零值比使用基于零的数组并四处寻找其零值要容易得多。这个理由几乎和以前一样愚蠢,但是后来,支持基于一个的数组的原始论点也很谬误。
第二,让我们记住,在处理数字时,您一次或一次地处理数学的机会很高,而当您处理数学时,机会是好的,您不会因为愚蠢的黑客而绕过过时的习惯。基于One的表示法也困扰着数个世纪的数学和日期,并且通过从错误中吸取教训,我们应努力在面向未来的科学(包括计算机语言)中避免使用它。
第三,对于与硬件绑定的计算机语言数组,分配一个由21个整数组成的C数组,并将指针向右移10个索引,您将获得一个自然的[-10至10]数组。这对于硬件而言是不自然的。但这是为了数学。当然,数学可能已过时,但我上次检查时,世界上大多数人都认为不是。
四,正如在其他地方已经指出的,即使对于离散位置(或距离减小为离散值),第一个索引也将为零,例如建筑物的地板(从零开始),递减的倒数计数(3、2、1,零!),地面高度,图像的第一个像素,温度(零开氏度,表示绝对零,或零摄氏度,如273 K的水冻结温度)。实际上,唯一真正开始的是传统的“ 第一,第二,第三等”方式。迭代符号,这自然地将我引向了下一点...
五个的下一个点(这自然遵循以前)是高层次的容器应被访问,而不是指数,而是通过迭代器,除非指数本身具有内在价值。我很惊讶您的“高级语言”拥护者没有提到这一点。在索引本身很重要的情况下,您可以将一半时间押在与数学有关的问题上。因此,您希望您的容器对数学友好,而不是像从1开始的“您的旧公历”那样禁用数学,并且需要反省的技巧才能使其工作。
程序员的说法是谬论,因为它不必要地将口头/书面语言习惯与自然地模糊的计算机语言(您不希望使指令模糊的)联系在一起,并且由于错误地归因于硬件他。她希望说服您,因为随着语言的抽象化程度越来越高,从零开始的数组已成为过去。
由于数学相关的原因,从零开始的数组也从零开始。不是出于与硬件相关的原因。
现在,如果这对您的同伴程序员是一个问题,请让他开始使用真正的高级构造进行编程,例如迭代器和foreach循环。
半开间隔构成良好。 如果您要交易0 <= i < lim
并且想要按n
元素扩展,则新元素的索引范围为lim <= i < lim + n
。从零开始的数组工作使拆分或连接数组或对元素计数时使算术更加容易。人们希望更简单的算法可以减少栅栏错误。
某些类型的数组操作会因基于1的数组而变得复杂,而对于基于0的数组则更为简单。
我曾经做过一些数值分析编程。我正在研究用FORTRAN和C ++编写的压缩稀疏矩阵算法。
FORTRAN算法具有很多a[i + j + k - 2]
,而C ++具有a[i + j + k]
,因为FORTRAN数组基于1,而C ++数组基于0。
数组中的索引实际上不是索引。它只是一个偏移量,它是距数组开头的距离。第一个元素在数组的开头,因此没有距离。因此,偏移量为0。
原因不仅是历史原因:C和C ++仍然存在并且被广泛使用,并且指针算术是使数组从索引0开始的非常有效的原因。
对于缺少指针算术的其他语言,第一个元素是位于索引0还是1只是一种约定,而不是其他任何约定。
问题在于,使用索引1作为其第一个元素的语言不是真空存在的,它们通常必须与经常用C或C ++编写的库进行交互(您猜对了)。
VB及其派生的样式一直遭受数组从0或1开始的困扰,并且长期以来一直是问题的根源。
底线是:您的语言认为第一个元素索引并不重要,只要它在整个过程中保持一致即可。问题在于,将1视为第一索引会使实际操作更困难。
And
并且Or
是按位已经什么都没有做与VB6,它是一个VB型语言的唯一合乎逻辑的解决方案。你也有AndAlso
和OrElse
你的逻辑运算。
And
并且Or
按位与VB6有关,因为它们在VB6中是按位的。丑陋的运算符AndAlso
和OrElse
应该按位进行运算,因为按位运算比逻辑运算要少得多。由于“向后兼容性”,语言上留下了许多丑陋的疣,例如即使默认情况下,ByVal仍然遍布整个地方。
从零开始的数组起源于C甚至汇编程序。使用C,指针数学基本上像这样工作:
为了说明这一点,假定int a[4]
位于0xFF00,地址为:
因此,对于从零开始的索引,addres数学很简单:
元素地址=数组地址+索引* sizeof(type)
实际上,C中的表达式都是等效的:
对于基于1的数组,数学(甚至是这样)稍微复杂一些。
因此,原因很大程度上是历史原因。
堆是基于1的阵列的优点之一。给定索引i,i的父级和左子级的索引为
PARENT
[ i ] = i ÷2
LCHILD
[ i ] = i ×2
但仅适用于基于1的阵列。对于从0开始的数组
PARENT
[ i ] =(i +1)÷2-1
LCHILD
[ i ] =(i +1)×2-1
然后,您具有i也是该索引(即[1,i ] 范围内的索引)的子数组大小的属性。
但最后没关系,因为您可以通过分配比平常多的一个元素并忽略第零个元素,将基于0的数组变成基于1的数组。因此,您可以选择在适当的时候获得基于1的数组的好处,并在几乎所有其他情况下保留基于0的数组以进行更清晰的算术运算。
我的感觉是,它是完全任意的。基于零或一的数组没有什么特别的。自从Visual Basic中解放自己(大多数情况下,有时我在Excel中做些小事情)以来,我还没有使用基于1的数组,而且... 是相同的。事实是,如果您需要数组的第三个元素,那么它只是一个实现细节,称为3或2。但是,处理数组的99%的工作只对两个绝对要点感兴趣:第一个元素和数量或长度。同样,这只是一个实现细节,第一个元素称为零而不是一个,或者最后一个元素称为count-1或count。
编辑:一些回答者提到基于1的数组更容易出现击剑错误。以我的经验,现在考虑一下,这是事实。我记得在VB中思考过,“这将奏效或会因为我被淘汰而失败。” 在Java中,这永远不会发生。尽管我以为自己会好起来,但一些回答者指出,从0开始的数组会导致更好的算术,甚至在您不必处理较低级别的lang时也是如此。
作为一个十岁以上的C / C ++程序员,在Pascal和Delphi方面有着非常深厚的背景,我仍然想念Pascal强大的数组绑定和索引类型检查,以及随之而来的灵活性和安全性。一个明显的例子是每个月的数组数据保存值。
帕斯卡:
Type Month = (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec);
Var Days[Month] of integer;
...
if Year mod 4 = 0 then // yes this is vastly simplified for leap years and yes i don't know what the comment marker is in pascal and no i won't go look it up
Days[Feb] := 29
else
Days[Feb] := 28;
在不使用+/- 1或“魔数”的情况下用C语言编写类似的代码非常具有挑战性。注意,像Days [2]和Days [Jan + Dec]这样的表达式根本无法编译,这对于仍在C或汇编语言中思考的人们来说可能是残酷的。
我不得不说Pascal / Delphi语言的很多方面我都不会错过,但是相比之下,基于C零的数组看起来确实只是“愚蠢”。
它以0而不是1开头的原因是,您可以将偏移量视为此元素距数组内存的起始位置有多远。这并不是说给我第0个元素,而是说给我一个从一开始就是0个元素的元素。
另一种看待它们的方式是(在大多数情况下)它们是等效的:
array[n]
*(array + n)
该标准永远不会更改的原因是因为C已有大约40年的历史了。没有令人信服的理由来更改它,如果进行了更改,则依赖于数组开头为0的所有现有代码都会被破坏。
array[n]
为n[array]
在C.这不是一个好主意,做它,它的混乱!但这是合法的(至少要达到C89),因为上面的标识和加法是可交换的。
包含一些原始位置/相对位置信息的代码使用从0开始的数组更加清晰。
例如:将向量复制到较大向量中定义位置的代码很麻烦,数组从1开始:
function copyAtPos (dest, vect, i):
for i from 1 -> vect.length do
dest[pos+i-1] = vect[i]
通过与以0开始的数组相对:
function copyAtPos (dest, vect, i):
for i from 0 -> vect.length-1 do
dest[pos+i] = vect[i]
如果您开始编写大卷积公式,则必须使用它。
为什么不2或3或20?它不像从零开始的数组那样容易理解或基于1的数组。为了切换到基于1的数组,那里的每个程序员都必须重新学习如何使用数组。
而且,当您处理现有数组中的偏移量时,它也更有意义。如果您从数组中读取了115个字节,则知道下一个块从115开始。依此类推,下一个字节始终是您读取的字节的大小。从1开始,您需要一直添加一个。
而且有时您确实需要处理数组中的数据块,即使是没有“真正”指针算法的语言。在Java中,您可以在内存映射文件或缓冲区中存储数据。在这种情况下,您知道块i的大小为* i。对于基于1的索引,它将位于块* i + 1。
使用基于1的索引,很多技术将要求整个位置+1。
使用基于1的数组,将一维数组转换为多维数组:
int w = 5, h = 5, d = 5;
int[] a1 = new int[w * h * d], new a2 = int[w,h,d];
for (int z = 1; z <= d; z++)
for (int y = 1; y <= h; y++)
for (int x = 1; x <= w; x++)
a1[x + (y - 1) * w + (z - 1) * h] = a2[x,y,z];
请注意,即使您的数组是基于1的,您的y和z索引也是基于0的(y-1,z-1)。在某些情况下,您无法避免从0开始的索引。为了保持一致性,为什么不总是使用基于0的索引?
我将在这里走出来,提出一些与整数“键控”数组不同的建议。
我认为您的同事正在物理上创建“集合”的一对一映射,而我们总是从1开始计数。我可以理解这一点,当您不花哨的时候,很容易理解一些代码当您在软件和物理世界之间一对一映射时。
我的建议
不要将基于整数的数组用于存储的内容,而应使用其他种类的字典或键值对。由于您不受任意整数的束缚,因此它们可以更好地映射到现实生活。它具有它的位置,由于在软件和物理世界之间映射概念1到1的好处,因此我建议您尽可能多地使用它。
即kvp['Name Server'] = "ns1.example.com";
(这只是一百万个可能示例中的一个)。
免责声明
当您使用基于数学的概念时,这绝对不起作用,主要是因为数学更接近于计算机的实际实现。在这里使用kvp集将无济于事,但实际上会使事情变得混乱,并使问题更加严重。我还没有考虑过某些情况,例如kvp或数组可能会更好。
最终的想法是在有意义的地方使用从零开始的数组或键值对,请记住,当您只有一把锤子时,每个问题开始看起来像钉子...
使用基于0的索引而不是基于1的索引的仅有两个(非常严重的理由)似乎避免了重新培训大量程序员,并且避免了向后兼容性。
在收到的所有答案中,我都没有看到针对基于1的索引的任何其他严重争论。
实际上,索引自然是从1开始的,这就是为什么。
首先,我们必须问:数组是从哪里来的?他们有真实世界的对等物吗?答案是肯定的:它们是我们在计算机科学中对向量和矩阵进行建模的方式。但是,向量和矩阵是在计算机时代之前就使用基于1的索引的数学概念(如今仍大多使用基于1的索引)。
在现实世界中,索引是以1为基数的。
正如Thomas所说,使用0基索引的语言实际上是使用offset而不是索引。使用这些语言的开发人员会考虑偏移量,而不是索引。如果事情清楚地说明了,那不是问题,但事实并非如此。许多使用偏移量的开发人员仍在谈论索引。而且许多使用索引的开发人员仍然不知道C,C ++,C#,...使用偏移量。
这是一个措辞问题。
(关于Diskstra的论文的注释- 正是我在上面所说的:数学家确实使用基于1的索引。但是Diskstra认为matematicians 不应该使用它们,因为这样某些表达式会很丑陋(例如:1 <= n <= 0 )。好吧,不确定他是否正确-进行这样的范式转换以避免那些异常的空序列似乎很麻烦,但最终结果还是...)
您是否曾经被真正提到1900年代的“ 20世纪”所困扰?好吧,这是您在使用基于1的数组时一直处理的繁琐事情的一个很好的类比。
考虑一个常见的数组任务,如.net IO.stream read方法:
int Read(byte[] buffer, int offset, int length)
我建议您这样做,以说服基于0的数组更好:
在每种索引样式中,编写一个支持读取的BufferedStream类。您可以为基于1的数组更改Read函数的定义(例如,使用下限代替偏移量)。不需要任何花哨的东西,只需使其简单即可。
现在,这些实现中哪一个更简单?哪一个位置的偏移量为+1和-1?那正是我所想。实际上,我认为,索引样式无关紧要的唯一情况是何时应使用非数组(如Set)之类的东西。
实际上,有几种不同的方法可以实现此目的:
最终,我认为语言使用基于0还是基于1的数组都没有关系。但是,我认为最好的选择是使用基于0的数组,原因很简单,因为大多数程序员都习惯于该约定,并且它与绝大多数已编写的代码一致。
但是,真正出错的唯一方法是与Visual Basic不一致。我当前正在维护的代码库分为基于0和1的数组。而且很难找出哪个是哪个。这将导致烦人的for循环:
dim i as integer, lb as integer, ub as integer
lb = LBound(array)
ub = UBound(array)
for i = lb to ub
'...
next
我更喜欢基于0的索引,因为模(以及用于模的AND运算符)对于某些值总是返回0。
我经常发现自己使用这样的数组:
int blah = array[i & 0xff];
使用基于1的索引时,我经常会出错。
如果不编写很多基于数组的代码(例如字符串搜索和各种排序/合并算法)或在单维数组中模拟多维数组,则很难保护0基。Fortran是基于1的,因此您需要大量的咖啡才能正确完成这种代码。
但这远不止于此。能够思考事物的长度而不是其元素的索引是一种非常有用的心理习惯。例如,在制作基于像素的图形时,将坐标视为落在像素之间而不是落在像素上更为清楚。这样,一个3x3矩形包含9个像素,而不是16个像素。
一个更牵强的示例是在解析或在表中打印小计之前的超前思想。“常识”方法说:1)获取下一个字符,令牌或表行,以及2)决定如何处理它。前瞻性方法表示1)假定您可以看到它,并确定是否需要它,以及2)如果您确实想要它,则“接受”它(使您可以看到下一个)。然后,如果您写出伪代码,则要简单得多。
另一个示例是如何在您无法选择的语言(例如MS-DOS批处理文件)中使用“ goto”。“常识”方法是将标签附加到要完成的代码块上,并对其进行标签。通常,更好的方法是将标签放在代码块的末尾,以跳过它们。这使其成为“结构化的”并且易于修改。
就是这样,已经有很多年了。更改甚至辩论,就像更改或辩论更改交通信号灯一样没有意义。让我们使blue = stop,red = go。
查看《 C ++数值食谱》中随着时间的变化。他们曾使用宏伪造基于1的索引,但在2001年版中放弃了并加入了这一行。在其网站www.nr.com上可能有启发性的材料说明了背后的原因。
顺便说一句,烦人的是在数组之外指定范围的变体。例如:python vs. IDL; a [100:200] vs a [100:199]获得100个元素。只是要学习每种语言的怪癖。更改一种语言以使其匹配另一种语言会导致这种咬牙切齿的咬牙切齿,而不是解决任何实际问题。
我更喜欢基于0的数组,因为正如其他人所提到的,它使数学变得更容易。例如,如果我们有一个包含100个元素的一维数组,它们模拟一个10x10网格,那么在r行col c中该元素的数组索引i是什么:
从0开始:i = 10 * r + c 从1开始:i = 10 *(r-1)+ c
并且,给定索引i,返回到行和列为:
从0开始:c = i%10 r =地板(i / 10) 从1开始:c =(i-1)%10 +1 r = ceil(i / 10)
鉴于上述数学在使用基于1的数组时显然更复杂,因此选择基于0的数组作为标准似乎是合乎逻辑的。
但是,我认为有人可以声称我的逻辑有缺陷,因为我认为会有理由在1D数组中表示2D数据。我在C / C ++中遇到过许多这样的情况,但是我必须承认,执行这样的计算需要某种程度上取决于语言。如果数组确实一直在为客户端执行所有索引数学运算,则编译器可以在编译时将基于M的数组访问简单地转换为基于0的访问,并向用户隐藏所有这些实现细节。实际上,任何编译时常量都可以用于执行相同的操作集,尽管这种构造可能只会导致难以理解的代码。
也许有一个更好的论点是,要使基于1的数组的语言中的数组索引操作数量最少,就需要使用上限函数执行整数除法。但是,从数学角度来看,整数除法应返回d个余数r,其中d和r均为正。因此,应使用基于0的数组来简化数学。
例如,如果要生成具有N个元素的查找表,则值x的当前值在数组中的当前值之前最近的索引为(大约,忽略结果是四舍五入之前的整数的值):
基于0且带下限:floor((N-1)* x / xRange) 基于1的楼层:楼层((N-1)* x / xRange)+ 1 基于1的ceil:ceil((N-1)* x / xRange)
请注意,如果使用四舍五入的标准约定,则基于1的数组需要附加操作,这是不希望的。编译器无法隐藏这种数学运算,因为它需要有关幕后情况的低级知识。
我敢打赌,程序员只是在日常思考中对基于0的数组的反直觉感到恼火,并且在争辩一种更直观的描述数组的方法。具有讽刺意味的是,作为人类,我们花了很多时间来提出“类”,以便我们可以在代码中以更人性化的方式描述事物,但是当查看0对1数组时,我们似乎挂了起来逻辑本身。
就计算机而言,从数学上讲0可能会更好,但是我觉得这里有一点遗漏。如果我们想用更人性化的方式来描述事物(例如类),为什么我们不希望在语言的其他部分也是如此?是不是同样不合逻辑或无效(或对此事优先考虑...)以使一种语言更易于人类理解和使用,因此,从广义上讲,它不那么容易产生逻辑错误,更容易加快生产可用作品的速度。PHP示例:
array(1 => 'January', 'February', 'March');
根据我们的要求给出基于1的数组。
为什么没有规范:
array('January', 'February', 'March');
例外是:
array(0 => 'Value for scenario where 0 *has* to be used as the key',
'value2', 'value3');
以PHP为例,我的赌注是80%的时间是基于1的数组作为默认语法,这将减少现实世界中的用例中的逻辑错误,或者至少不会平均导致更多的逻辑错误,同时使它变得更多在编码器上更容易更快地生成可用代码。记住,我假设在需要时仍可以选择array(0 =>'value'),但同时也假设在大多数情况下,有一些更接近真实世界的描述是可行的。
从该角度来看,这听起来确实不是太过牵强。当使用界面时,无论是操作系统还是程序员的语言,我们都更接近人类的思维和习惯来设计界面,在大多数情况下我们会更快乐,而人类与计算机之间的误解也就更少了(人类逻辑-错误),更快的生产等。如果在现实世界中有80%的时间在进行列表或计数时我用1来描述事物,那么计算机理想情况下应该以很少的信息来理解我的意思,或者与我通常的描述方式有所不同。简而言之,我们可以对现实世界建模的越近,抽象的质量就越高。因此,他想要的绝不是愚蠢的,因为这是最终目标,并且将证明需要更多的抽象。计算机仍然可以最终将其视为基于0的数组的特殊用法。只要计算机以一种更简单,更直观的方式描述我想要的东西,并且随着时间的推移减少错误,我就不必在乎计算机如何解释它。
所以,那是我的两分钱。我严重怀疑他在说什么或被解释为他的意思。他可能的意思是,“我讨厌用一种不太直观的方式告诉计算机我想要什么。” :)我们不是所有人吗?大声笑。