左闭右开,这是最基本的代码准则。
区间或范围在编程世界中无处不在。典型的例子是选择开始和结束日期,就像你在Airbnb上预订房间或预订航班时那样。这样的例子有很多:从切片JS数组,到Java的List#sublist甚至SQL的LIMIT运算符,区间无处不在。
你有没有想过为什么它们的设置总是[closed, open)而不是[closed, closed]?
什么是 [closed, open) 区间?
一个闭-开区间,通常表示为[a, b),这是一个缩写形式,用于表达a <= x < b的所有值的集合,从a开始并包括a,到b但不包括b,例如,如果我们使用整数,那么[0, 5) == 0, 1, 2, 3, 4。
一个闭-闭区间,通常表示为[a, b],是包含最后一个值的区间,例如[0, 5] == 0, 1, 2, 3, 4, 5。
永远不要使用[closed, closed]区间
几年前,我有幸在一个广泛使用[closed, closed]区间的系统中工作。该系统在大多数情况下能良好运行,但也需要有大量的笨重的代码来处理一些少数突发情况。以下是我们必须应对的一些情况:
空的区间
假设你想描述一个从时间T=1开始的零长度时间区间(即空区间)。这对于闭-开区间来说很简单,简单地说就是[T, T),但对于闭-闭区间……就不是那么回事了。你可以尝试[T, T-1],但这有点问题,如果T是十进制数那么将无法实现。
按时间拆分
想象一下,你想对注册到你站点的用户按时间对他们进行分组,例如每小时分一组。这本质上是将一天中的24小时以1小时为区间“拆分”的问题。
通过[closed, open),你将得到一个很好的序列:[0, 1)、[1, 2)、[2, 3)、[3, 4)……[23, 24)。
通过[closed, closed],你将得到一个奇怪的序列:[0, 0:59:59]、[1, 1:59:59]……[23, 23:59:59]。请注意,这实质上会在你的整个系统中强制执行一个精确度。在0:59:59和1之间有用户注册么?这些用户会被落下。
计算区间的长度
每当涉及区间时,另一个常见的任务是计算它们的长度。
对于[a, b)很简单,它只是b-a。
但对于[a, b],会遇到一些边缘情况,例如如前所述[a, a-1]的长度可能为0,或者如果a < 1,则它可能为负。
你期望从正确实现的区间中获得的另一个属性是,将区间分成两半应该会产生两个小区间,这两个小区间长度加起来等于原始区间的长度。
例如,将一天中的小时数分成两个区间,应该得到长度为12的2个区间,即12+12 = 24。但使用[a, b]区间时会丢失此属性。
最后的想法
在我写这篇文章时,我发现了ol’ Edsger W. Dijkstra在1982年写的一篇简短的笔记,讲述了他为什么更喜欢[closed, open)区间。我不会用细节来烦你,但我只想说施乐帕克研究中心(Xerox PARC)的那些聪明人尝试了它们,发现[a, b]范围会导致错误和更复杂的代码。
我希望这篇简短的文章能让你认识到使用[closed, closed]区间的分险和陷阱。我猜测,人们有时喜欢使用[closed, closed]区间的原因,是它们看起来漂亮且对称,而且大部分时间能正常工作。
只有在极端情况下,它们才会开始崩溃。但这正是你应该评估设计好坏的关键:对其边缘情况进行测试。
推荐阅读:
分布式实验室策划的《Go实战集训营》正式上线了。这门课程通过5天线上集训,4个练习项目,40天课后辅导,和学员一起一步一步基于Go常用框架搭建起一个可以访问MySQL、Redis和第三方服务的Go服务,并进行线上调试,排查问题。培训重实战、重项目、更贴近工作,边学边练,1月7日正式开课,早鸟价倒计时3天!