Ruby中的Range类型
Ruby Range类型
Range是一个序列范围。使用s..e
或s...e
或Range#new()
来创建范围对象(一般使用rng来表示)。
它们适用于整数值(不能是float类型)、字母。事实上,只要某个类定义了<=>
方法以及succ()
方法,那么自动就能支持Range。
1 | # range字面量创建Range |
使用两点..
创建的是包含结束边界元素的,使用三点...
创建的是不包含结束边界元素的。如果Range.new()
方法的第三个参数设置为true(默认new方法的第三个参数exclude_end=false
),则创建的range也是不包含结束边界的。
可以使用exclude_end?
方法来检测一个rng对象是否包含结束边界。
1 | (1..10).exclude_end? #=> false |
另外一点需要注意,反序Range是合理但无值的,它不适合直接使用,但适用于索引取值,例如"abcde"[2..-1]
。
无上界和无下界的Range
有一种特殊的Range对象,它们是没有结束边界的。换句话说,它的上界为无穷。
1 | 1.. |
需要注意两点:
1 | (1..) == (1..) #=> true |
无上界的范围有时候非常实用,特别是在某些不知道上边界的时候。例如,取数组第一个元素到最后一个元素,但是最后一个元素的index是不固定的,使用无上界的Range非常容易取得这区间的元素:
1 | [1,2,3,4,5][2..] #=> [3, 4, 5] |
在Ruby 2.7中开始支持无下界Range(之前的版本并不支持):
1 | arr = [3,2,1,5,4,6] |
指定步长
rng.step(N)
方法或百分号rng % N
可以设置一个Range对象rng的步长为N。
但是,step一般需要结合语句块来使用,否则的话返回的是一个Enumerator。而%
的方式无法结合语句块,只能先返回它的Enumerator,再来手动迭代。
例如:
1 | r = ((1..10).step(2)) #=> ((1..10).step(2)) |
Range对象迭代
Range类自身只实现了一个each()方法。
1 | (1..10).each {|n| puts n} |
但它mix-in了Enumerable模块,所以这个模块中的方法都能使用。
但是注意,Range对象如果起点是浮点数,那么是不可迭代的,因为中间的浮点数是无穷多。但终点是浮点数与否则无所谓,它会按整数方式迭代到终点截断小数位后的整数。
1 | (1..3.5).each {|x| puts x} # 1 2 3 |
Range的一些常用操作
size()
:返回Range对象中包含的元素个数to_a()
或entries()
:它们等价,返回Range对象各元素组成的数组begin()
:返回Range对象的第一个元素end()
:返回Range对象的最后定义元素first()
:返回Range对象的第一个或前N个元素,用法见下文示例last()
:返回Range对象的最后一个或N个元素,用法见下文示例
begin()
和end()
分别返回Range对象的首尾定义元素,不论它是否包含结束边界。例如:
1 | (1..10).end #=> 10 |
对于first()
和last()
,均可指定参数或不指定参数。不指定参数时分别等价于begin()
和end()
,指定参数N时,返回实际包含的前N个或最后N个值。
1 | (10..20).first #=> 10 |
Range对象相等性判断
rng1 == rng2
和rng1.eql? rng2
用于比较两个Range对象是否相等。
比较具有两方面:
- 分别使用
==
或eql?
来比较rng的头尾元素是否相等 - rng的边界性质是否一样,包含边界和不包含边界是不一样的
换句话说,只有头尾两元素相等且边界性质相同,才是相等的范围。
Range成员判断
非常常用的一个成员判断方式是Range实现的===
方法。
1 | (1..10) === 3 #=> true |
由于case的等值判断语法中是使用===
来判断的,所以对于range而言,可直接使用case语句判断一个对象是否在range范围内。
1 | case 79 |
还有include?()
方法和member?()
,它们等价,也是用来判断是否是Range对象的成员。
1 | (1..10).include?(3.1) #=> true |
此外,还有cover?()
方法判断另一个Range是否是该Range的子集。只不过它只是比较首尾两个元素而已,所以有时候的结果可能出乎想象。
1 | (1..10).cover? 3 #=> true |
注意上面最后一个测试结果,『cc』本不是"a".."z"
的成员,但是却返回true,因为它在做首尾比较的时候,发现"a"<="cc"
且"cc"<="z"
均为真,于是就返回真。
最大和最小值
1 | max → obj |
返回Range对象中最大、最小的值,如果给了参数n,则返回n个最大、最小值组成的数组。默认使用<=>
比较各元素。
如果给了语句块,则使用语句块的逻辑来比较而不是默认的a<=>b
,其中a和b是遍历时每次所取的元素。
如果首元素大于尾元素,或者在不包含边界时首尾元素相等,那么直接返回nil或空数组(如果给了n参数)。
1 | (1..10).max #=> 10 |
自定义支持Range的类
只要某类重写了<=>
和succ()
方法,就支持range操作。其中succ()用于每次获取下一个元素,<=>
用于判断每次所获取到的元素是否到达了结束边界。
例如,在官方手册上定义了一个类Xs,该类中重写了succ和<=>
方法,于是就支持range操作。先把该方法中的inspect注释掉。看看它构造出的范围是什么样的。
1 | # represent a string of 'x's |
构造一个Xs对象组成的序列范围:
1 | rng = Xs.new(3)..Xs.new(6) |
这样的结果看上去显然是不好看的。于是定义下inspect()
方法,使其人类可读。在这里to_s是用不上的,因为对于range来说,它只需要显示对象信息。to_s是在转换成字符串时使用的。
1 | r = Xs.new(3)..Xs.new(6) #=> xxx..xxxxxx |
Ruby中的flip-flop
flip-flop借鉴自Perl,flip-flop有些时候非常实用,特别是在处理文本数据时,它返回flip为真和flop为真中间范围内的数据。
在Ruby中flip-flop的语法flip..flop
和flip...flop
看起来像是Range,但只是运算符相同,它本质并不是Range,它支持..
和...
,但效果是等价的。
1 | cat c.txt |
Ruby之前的版本曾经想要废弃flip-flop语法,在Ruby 2.6版本使用flip-flop时也会给出警告信息,但因很多程序员反对废弃,Ruby 2.7又添加回来了。