索引区间格式问题 |
|
整个问题的根源在于编程的历史。C 等一些语言的实现细节是数组项通过指针 + 偏移地址来引用。因此,偏移量存在于实现级别,这导致语言设计者可以选择将该方案转移到语言本身,以便程序员必须适应它,或者让编译器或解释器从所有索引中减去 1,以便(通常是人类)程序员可以使用序数而不是偏移量。C 语言没有选择这种方案,因此整个 C/C++/Java/Python/Ruby 家族都受到了影响。由于 C 语言非常成功,想要让自己的“宝贝”得到广泛使用的语言设计者必须考虑到这个事实上的标准——除了他们也是 C 程序员之外。现在,声称一个特性是“自然的”仅仅因为它是一个标准,这本身就是一个逻辑错误。
数组、列表、序列和表格是有序集合。人类使用序数——第一、第二、第三(或 #1、#2、#3)——来指向集合中的一个元素(哪一个?)。相反,他们使用基数——1、2、3——来计算元素的数量(有多少?)。不幸的是,编程语言以英语为主,而这种语言将两种数字类型都称为“数字”(与德语中的 Nummer/Zahl 或法语中的 num�ro/nombre 相比)。尽管如此,所有语言都对两者进行了区分。在人类生活的各个领域,除了编程的一个子领域之外,序数都用于指向事物。寻找公元 0 年,书架上的第 0 本书,或贝克街 0 号 ;-)。此外,序数似乎出现得更早,这就是“零”这个基数在人类历史上如此年轻的原因。语言证明了这一点:人们说“没有人”、“从不”和“没有东西”,而不是使用“零”这个词。所有这一切都是为了说明,在人类层面上,程序员友好的选择是使用序数而不是偏移量来引用有序集合中的项目。
a[(i - 1)%n + 1]
。一天的第一小时是 12 点,最后一小时是 11 点(看来他们还算有脑子,让 1 点从午夜开始一小时后开始,但到了午夜本身就乱成一团了)。十个的列表总是包含一个两位数;这彻底搞糊涂了学习数数和读到 10 的孩子,当他们不得不学习零的时候,他们就更加糊涂了。一次旅行的第一公里从零公里开始。 �MarceloCantos?
类似地,闭区间恰好更自然或更直观。在日常生活中,“从第一个到第三个”在任何情况下都有包含的含义。因此,使用半开区间首先需要进行一项心理工作,最终会变得自动化。这并不意味着这种语法效率较低,也不意味着它不合理,而仅仅意味着对于非专业人士来说,它不那么人性化。一些程序员似乎为这些导致新手出错的深奥功能感到自豪 ;-)。
这里有必要区分过程和人类语义层级。在过程层面上,两种方案的功能都相同,两种语义都一致。在人类层面上,0-base/半开方案需要从过程层进行一种“转码”。因此,如果有人同意源代码(以及例如数学表达式)首先要让人类阅读,而不是机器阅读,那么这种方案就是一个较差的选择——只有在另一种方案因任何原因被证明不可能的情况下,才应该选择它。请注意,Pascal 虽然和 C 一样古老,但它采取了相反的方式——这并非巧合,因为它被设计成适合教育。
在日常工作中,一旦习惯了其中一种约定,两种方案都证明是可用的。当程序员讨论这个问题时,他们往往会固执地坚持自己的个人习惯,也就是他们通过日常使用形成的思维习惯。由于大多数使用的语言遵循 C 约定,因此有很多论据支持它。现在,仔细观察,这些话恰好是错误的,因为它们是逻辑上的理由:在理性的表达背后,它们是 *意见*。
例如,认为 [n,n)
是表示 0 长度区间的最佳方式的论点或多或少是毫无意义的;表示空序列的唯一明智方法是 []
。[5,5)
和 [5,4]
在语义上都是荒谬的。现在,在 *运行时*,序列切片可能为空——这是另一个层次:程序员在 *设计时* 并不处理它。
[]
来表示空区间很笨拙。计算机程序通常会使用具有两个端点的数据结构: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?
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?