索引区间格式问题

lua-users home
wiki

本文讨论了 1-based 索引和闭区间切片与 0-based 索引和右开区间切片相结合的问题。在我看来,其他解决方案并不一致。

[最初作者的说明:虽然本文首先表达了我对该主题的观点,但作为维基页面,它旨在进一步阐述(也需要润色,因为我不是英语母语人士!)。然而,我已经思考和讨论过这个问题一段时间了,特别是因为我参与了培训;所以你可能也需要考虑一下,然后再开火。编辑后删除此说明。--DeniSpir (denis dot spir at free dot fr), 2008 年 10 月 8 日。]

战争的起源?

整个问题的根源在于编程的历史。C 等一些语言的实现细节是数组项通过指针 + 偏移地址来引用。因此,偏移量存在于实现级别,这导致语言设计者可以选择将该方案转移到语言本身,以便程序员必须适应它,或者让编译器或解释器从所有索引中减去 1,以便(通常是人类)程序员可以使用序数而不是偏移量。C 语言没有选择这种方案,因此整个 C/C++/Java/Python/Ruby 家族都受到了影响。由于 C 语言非常成功,想要让自己的“宝贝”得到广泛使用的语言设计者必须考虑到这个事实上的标准——除了他们也是 C 程序员之外。现在,声称一个特性是“自然的”仅仅因为它是一个标准,这本身就是一个逻辑错误。

C 语言中使用零索引是一个选择,许多(可能是大多数)程序员至今都同意这一点,你不能仅仅假设他们继续偏爱它仅仅是因为熟悉或约定俗成(除非你当然把你的结论作为前提)。你最后一句话,以及“宝贝”之类的词语,不公平地用同一把刷子抹黑了你观点的所有反对者。 �MarceloCantos?

运动

人类是人类

数组、列表、序列和表格是有序集合。人类使用序数——第一、第二、第三(或 #1、#2、#3)——来指向集合中的一个元素(哪一个?)。相反,他们使用基数——1、2、3——来计算元素的数量(有多少?)。不幸的是,编程语言以英语为主,而这种语言将两种数字类型都称为“数字”(与德语中的 Nummer/Zahl 或法语中的 num�ro/nombre 相比)。尽管如此,所有语言都对两者进行了区分。在人类生活的各个领域,除了编程的一个子领域之外,序数都用于指向事物。寻找公元 0 年,书架上的第 0 本书,或贝克街 0 号 ;-)。此外,序数似乎出现得更早,这就是“零”这个基数在人类历史上如此年轻的原因。语言证明了这一点:人们说“没有人”、“从不”和“没有东西”,而不是使用“零”这个词。所有这一切都是为了说明,在人类层面上,程序员友好的选择是使用序数而不是偏移量来引用有序集合中的项目。

人类也用 0 来表示小时的第一分钟。还要考虑一下我们被基于一的计数方式强加的愚蠢行为。新的千年始于 2001 年 1 月 1 日。 a[(i - 1)%n + 1] 。一天的第一小时是 12 点,最后一小时是 11 点(看来他们还算有脑子,让 1 点从午夜开始一小时后开始,但到了午夜本身就乱成一团了)。十个的列表总是包含一个两位数;这彻底搞糊涂了学习数数和读到 10 的孩子,当他们不得不学习零的时候,他们就更加糊涂了。一次旅行的第一公里从零公里开始。 �MarceloCantos?

你关于“没有人”、“从未”和“没有”这些词的论点是非 sequiter。这些都是基数词,而不是序数词。也许奇怪的是,这些词被用来描述基数为零的集合,但这与序数是否应该从零开始的问题关系不大。 �MarceloCantos?

类似地,闭区间恰好更自然或更直观。在日常生活中,“从第一个到第三个”在任何情况下都有包含的含义。因此,使用半开区间首先需要进行一项心理工作,最终会变得自动化。这并不意味着这种语法效率较低,也不意味着它不合理,而仅仅意味着对于非专业人士来说,它不那么人性化。一些程序员似乎为这些导致新手出错的深奥功能感到自豪 ;-)。

同样,使用“自豪”和“深奥”等感情用词是一种人身攻击修辞手法,试图通过非理性论证的方式来影响读者。 �MarceloCantos?

一些程序员发现半开区间是一个更合理的系统,它比闭区间产生更可靠的代码,而且这种可靠性胜过为了迎合非专业人士的“直觉”而遵循千年来的错误。 �MarceloCantos?

这里有必要区分过程和人类语义层级。在过程层面上,两种方案的功能都相同,两种语义都一致。在人类层面上,0-base/半开方案需要从过程层进行一种“转码”。因此,如果有人同意源代码(以及例如数学表达式)首先要让人类阅读,而不是机器阅读,那么这种方案就是一个较差的选择——只有在另一种方案因任何原因被证明不可能的情况下,才应该选择它。请注意,Pascal 虽然和 C 一样古老,但它采取了相反的方式——这并非巧合,因为它被设计成适合教育。

程序员也是如此

在日常工作中,一旦习惯了其中一种约定,两种方案都证明是可用的。当程序员讨论这个问题时,他们往往会固执地坚持自己的个人习惯,也就是他们通过日常使用形成的思维习惯。由于大多数使用的语言遵循 C 约定,因此有很多论据支持它。现在,仔细观察,这些话恰好是错误的,因为它们是逻辑上的理由:在理性的表达背后,它们是 *意见*。

两种方案都可能可用,但半开区间本质上更简单、更合理,并且不太容易出现“越界”错误。 �MarceloCantos?

例如,认为 [n,n) 是表示 0 长度区间的最佳方式的论点或多或少是毫无意义的;表示空序列的唯一明智方法是 [][5,5)[5,4] 在语义上都是荒谬的。现在,在 *运行时*,序列切片可能为空——这是另一个层次:程序员在 *设计时* 并不处理它。

首先,一个小小的争论:这个论点可能或多或少有道理,但说它毫无意义是毫无道理的。 �MarceloCantos?

使用 [] 来表示空区间很笨拙。计算机程序通常会使用具有两个端点的数据结构:struct range { int start, end; };。用于求两个这样的区间的交集的代码非常简单:range intersect(range a, range b) { range r = { max(a.start, b.start); min(a.end, b.end); if (r.end < r.start) r.end = r.start; return r; },基数也是如此:int count(range r) { return r.end - r.start; },以及空函数:bool empty(range r) { return r.start == r.end; }。一旦你要求对空区间使用不同的表示,所有东西都会突然变得更加复杂,并且会消耗更多的 RAM 和 CPU。你还会发现,你必须在代码中大量使用 + 1- 1 来纠正各种“越界”问题(例如,计数现在需要 r.end - r.start + 1 以及显式地测试空状态)。 �MarceloCantos?

对于连续量,闭区间非常笨拙。如何表达对应于一天的时间戳范围?半开区间可以轻松地做到这一点:[2010-01-01, 2010-01-02)。闭区间需要这样做:[2010-01-01, 2010-01-01 23:59:59],这至少有两个问题:1) 它必然假设一个量子;2) 它是表达一天这个简单概念的一种不自然且复杂的方式。半开区间也很容易拼接成连续的非相交子区间:[e, pi) 可以拼接成 [e, 3) 和 [3, pi)。在一般情况下,闭区间无法做到这一点,即使有量子,[2010-01-01, 2010-01-01 11:59:59] 和 [2010-01-01 12:00:00, 2010-01-01 23:59:59] 是否连续也不明显(量子是一秒吗?)。虽然这些问题中的许多在诸如数组索引之类的离散应用中更容易处理,但半开区间仍然更容易使用。举个简单的例子,[a, b) 和 [b, c) 显然是连续的,而 [a, b] 和 [c, d] 需要进一步检查代码。此外,用数组 [a, b, c, ..., z] 表示半开区间的 n 路拼接 [a, b), [b, c), ..., [y, z) 比较容易,而用闭区间的 n 路拼接,则需要小心记住是存储每个拼接的开始还是结束。 �MarceloCantos?

还要考虑这样一个事实,即半开区间的“长度”是 end - start,无论该区间是连续的还是离散的,而闭区间的“长度”公式对于连续区间和离散区间是不同的。作为一个实用且相当常见的例子,半开日期范围内的天数计算方式相同,无论端点是日期还是时间戳。这就是为什么我倾向于使用 date1 <= d AND d < date2 而不是 SQL 的 BETWEEN 运算符;我永远不必考虑所讨论的变量是日期还是时间戳,也不必担心维护人员是否会将类型从一种切换到另一种。 �MarceloCantos?

[5,5) 是一个退化情况,这种情况下通常非常有用。究竟是什么让它变得荒谬? �MarceloCantos?

和平之光

在 21 世纪,这种时间和精力的损失是否应该继续发生?至少有两条途径可以摆脱这个问题。

显式语法

正如一些人指出的那样,BASIC 允许显式表达数组索引方案。例如,array(0,10) 将定义一个以零为基的索引范围。从类似的角度来看,可以使用两种常见的数学语法显式地使用半开区间: [a,b) [a,b[ 。此选项的额外优势是消除了容易出错的格式,因为使用的方案是显式写入的。所有这些对人类读者来说都是好的,但问题仍然是如何正确地解释以自己不习惯的约定编写的表达式。以下建议解决了这个问题。

编辑器定制层

作为程序员,我们都熟悉编辑器自定义功能,比如缩进选项,让我们可以选择使用制表符还是空格,以及缩进的宽度。需要注意的是,这适用于加载/读取和编辑/保存源代码文件。无论作者使用什么选项,我们都可以使用自己喜欢的约定来读取和编辑代码。无论我们使用什么选项,其他开发者也可以使用自己的选择来读取和编辑相同的代码。现在,文件可以根据任何标准规范保存,无论如何。这是一种通过编辑器自定义层实现的前景/背景区分。很棒!

现在,为什么不将此原则扩展到*任何*语言特性呢?例如,有人可能更喜欢去掉赋值语句中的 '='(语义上是错误的),用 ':' 代替,而只使用 '=' 来表示逻辑相等。加载文件时,编辑器应该根据这些偏好显示代码,无论保存时使用的标准是什么。

索引和切片也是如此:C 程序员会设置 0 为基准的半开区间切片,并以这种方式显示代码,无论底层保存标准是什么。习惯使用 Delphi 的程序员会选择相反的方案,无论如何。

[你觉得怎么样?我最近主要用 Python 编程,而且大多数针对这种语言的 IDE 也是用 Python/wxPython 编写的。所以,当我有时间的时候,我打算在编辑器中实现这个功能...]

“前方有龙。”编辑器如何将 a[len - 1 - i] 转换为 1 为基准的索引?它会简单地将索引包装起来,例如:a[(len - 1 - i) + 1],还是会尝试解析内容,寻找要删除的 "- 1" 或要更改的 "+/- <constant>",例如:a[len - i]?如果代码未完成:a[len - ],或者只是反常:a[~i & 0xff] /* len == 256 */?如何处理 C++,它允许你重载 operator []?还要考虑程序员经常在讨论代码时共享屏幕,这会导致严重的混乱。 �MarceloCantos?

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2020 年 6 月 2 日下午 3:50 GMT (差异)