Ruby 集合指南(1):数组
编程中有大量的排序和搜索。在比较老的C语言中,你可能要自己写数据结构和算法才能完成这些任务。然而,在Ruby中为了能更加关注任务处理已经将这些构造为抽象的方式。
下面这些指导将介绍这些抽象结构。这些指导不可能很全面-光讲Ruby集合就能写一本书了-但我能广泛撒网,而且我覆盖了我认为做为一个Ruby程序员将经常要计算的方面。它们可以被分为4个方面:
- 数组和迭代
- 哈希,集合和排列
- 枚举和计数器
- 技巧和提示
这个指导例子非常多。我觉得学习这些最好的方式是打开irb命令行跟着例子学习,通过你自己变化多端的方式来创建程序。
数组
数组是Ruby集合中一匹战马。很多在集合中操作方法的返回值都是数组,即使原本集合不是数组。事实上,它们并不是真正的数组,它是一种万能的数据结构。你可以把它们用做集合,栈,或着队列。其功能和Python list相似。
创建
Ruby数组的创建和一些动态语言创建方式类似。
>> numbers = [1, 0, 7]
>> numbers[2]
=> 7
>> numbers.size
=> 3
数组中的类型不必一样。它们是多样的。
>> data = ["bob", 3, 0.931, true]
因为Ruby是完全基于对象的,数组将描述为一个对象而不只是特殊的翻译规则。这意味着你可以和其它对象一样来构造数组。
>> numbers = Array.new([1,2,3])
=> [1, 2, 3]
数组的构造函数可以传入一个开始的大小,但是它可能不会和你想象的一样工作。因为Ruby数组是动态的,没必要提前申请足够的空间。当你用Array.new的方式来传入一个数字,一个将包含nil的对象将被创建。
>> numbers = Array.new(3)
=> [nil, nil, nil]
尽管nil是个独立的对象,它也在集合中和其它对象一样占据位置。所以当你给数组添加一个nil对象,它会在数组末尾添加。
>> numbers << 3
=> [nil, nil, nil, 3]
>> numbers.size
=> 4
如果你在Array.new中传入第二个参数,数组将会用它代替nil进行初始化。
>> Array.new(3, 0)
=> [0, 0, 0]
>> Array.new(3, :DEFAULT)
=> [:DEFAULT, :DEFAULT, :DEFAULT]
在增加标准文字方面,Ruby提供一些别的通过%来实现的语法捷径。符号。
>> ORD = "ord"
>> %WThis is an interpolated array of w#ORDs
=> ["This", "is", "an", "interpolated", "array", "of", "words"]
>> %wThis is a non-interpolated array of w#ORDs
=> ["This", "is", "a", "non-interpolated", "array", "of", "w\#ORDs"]
数组目录
很多语言中如果你试图访问不存在的数组索引会抛出异常。如果你试图读一个不存在的索引,Ruby返回nil。
>> spanish_days = []
>> spanish_days[3]
=> nil
如果你写一个不存在的索引,Ruby会在其索引位置写一个nil。
>> spanish_days[3] = "jueves"
=> [nil, nil, nil, "jueves"]
>> spanish_days[6] = "domingo"
=> [nil, nil, nil, "jueves", nil, nil, "domingo"]
在很多语言中,如果你访问一个负索引也会引发错误。而Ruby认为负索引是数组末尾的开始,反向增加。
>> ["a","b","c"][-1]
=> "c"
>> ["a","b","c"][-3]
=> "a"
如果你提供一个不存在,负数组索引,结果都是不存在-nil
>> ["a","b","c"][-4]
=> nil
数组分类
Ruby数组中另一个有用的特性就是可以对元素进行分类。然而,在Ruby可以用比较苛刻和许多不同的方式来指定元素的分类。
>> letters = %wa b c d e f
=> ["a", "b", "c", "d", "e", "f"]
>> letters[0..1]
=> ["a", "b"]
>> letters[0, 2]
=> ["a", "b"]
>> letters[0...2]
=> ["a", "b"]
>> letters[0..-5]
=> ["a", "b"]
>> letters[-6, 2]
=> ["a", "b"]
下面是对例子的介绍
- letters[0..1] – 返回从0到1的元素
- letters[0, 2] – 返回从0位置开始后的2个元素
- letters[0...2] – 返回从0直到位置2的元素
- letters[0..-5] – 返回从0到倒数位置为5的元素
- letters[-6, 2] – 返回从倒数第6个开始2个元素
如果你刚开始学习Ruby,你可能想要知道这些是怎样实现的。访问数组的最好方式就是用[]方法。
>> letters.[](0..1)
=> ["a", "b"]
另外,0..1不过是一个伪装的Range对象。你可以用class方法来验证。
>> (0..1).class
=> Range
所以实际上是Range对象给Array[]传递目标分类。
>> letters.[](Range.new(0,1))
=> ["a", "b"]
如果你喜欢,这种在Ruby中以基于面向对象的方式可以创建出很漂亮的东西。那么如果你想要用数字来带英文字符呢?
numerouno gem可以解析英文数字。
$ gem install numerouno
>> require 'numerouno'
>> "one-hundred sixty three".as_number
=> 163
用numerouno你可以让数组类拥有英文索引了。
class EnglishArray < Array
def [](idx)
if String === idx
self.at(idx.as_number)
end
end
end
>> arr = EnglishArray.new(["a","b","c"])
>> arr["one"]
=> "b"
变形
记得我之前说过Ruby的Array类型是万能结构吗?下面是你可以用在Array上操作的例子。
增加元素
>> [1,2,3] << "a"
=> [1,2,3,"a"]
>> [1,2,3].push("a")
=> [1,2,3,"a"]
>> [1,2,3].unshift("a")
=> ["a",1,2,3]
>> [1,2,3] << [4,5,6]
=> [1,2,3,[4,5,6]]
删除元素
>> arr = [1,2,3]
>> arr.pop
=> 3
>> arr
=> [1,2]
>> arr = ["a",1,2,3]
>> arr.shift
=> "a"
>> arr
=> [1,2,3]
>> arr = [:a, :b, :c]
>> arr.delete(:b)
=> :b
>> arr
=> [:a, :c]
>> arr.delete_at(1)
=> :c
>> arr
=> [:a]
添加数组
>> [1,2,3] + [4,5,6]
=> [1,2,3,4,5,6]
>> [1,2,3].concat([4,5,6])
=> [1,2,3,4,5,6]
>> ["a",1,2,3,"b"] - [2,"a","b"]
=> [1,3]
布尔操作
>> [1,2,3] & [2,3,4]
=> [2,3]
>> [1,2,3] | [2,3,4]
=> [1,2,3,4]
>> arr1 = [1,2,3]
>> arr2 = [2,3,4]
>> xor = arr1 + arr2 - (arr1 & arr2)
=> [1,4]
移动元素
>> [1,2,3].reverse
=> [3,2,1]
>> [1,2,3].rotate
=> [2,3,1]
>> [1,2,3].rotate(-1)
=> [3,1,2]
安全提示
>> arr = [1,2,3]
>> arr.freeze
>> arr << 4
=> RuntimeError: can't modify frozen Array
给String添加元素
>> words = ["every","good","boy","does","fine"]
>> words.join
=> "everygoodboydoesfine"
>> words.join(" ")
=> "every good boy does fine"
删除嵌套
>> [1,[2,3],[4,["a", nil]]].flatten
=> [1,2,3,4,"a",nil]
>> [1,[2,3],[4,["a", nil]]].flatten(1)
=> [1,2,3,4,["a", nil]]
删除副本
>> [4,1,2,1,5,4].uniq
=> [4,1,2,5]
切割
>> arr = [1,2,3,4,5]
>> arr.first(3)
=> [1,2,3]
>> arr.last(3)
=> [3,4,5]
查询
>> ["a","b","c"].include? "d"
=> false
>> ["a", "a", "b"].count "a"
=> 2
>> ["a", "a", "b"].count "b"
=> 1
>> [1,2,[3,4]].size
=> 3
迭代
迭代可以说是Ruby真正亮点。在很多语言中迭代感觉都是很笨拙。然而,在Ruby中你不会感到你需要写一个典型的for循环。
在Ruby中迭代的核心构造器是each方法。
>> ["first", "middle", "last"].each |i| puts i.capitalize
First
Middle
Last
尽管each是Ruby迭代中的核心,但还有很多其他的方式。比如,你可以通过一个reverse_each来反向迭代集合。
>> ["first", "middle", "last"].reverse_each |i| puts i.upcase
LAST
MIDDLE
FIRST
另一个需要处理的方法是each_with_index可以在第二个参数中传入一个目前处理的索引。
>> ["a", "b", "c"].each_with_index do |letter, index|
>> puts "#letter.upcase: #index"
>> end
A: 0
B: 1
C: 2
那each方法是怎么工作的呢?最好的理解方式就是你创建自己的each。
class Colors
def each
yield "red"
yield "green"
yield "blue"
end
end
>> c = Colors.new
>> c.each |i| puts i
red
green
blue
yieId方法调用块来给each传递数据。这对于Ruby初学者来说把注意力都放这里可能有些难。那就认为yieId是调用一个匿名代码体,然后你在里面提供了方法yieId。在前面的例子中,yieId被调用3次,所以"|i| puts i"执行了3次。
局部迭代
循环中一个比较好的事情是开始和结束点可以指定。each方法可以迭代整个集合。
如果你只想迭代集合的某一部分,至少可以有如下的两种方法来实现:
将集合切开然后迭代切过后的集合
>> [1,2,3,4,5][0..2].each |i| puts i
1
2
3
用Range来生成索引
>> arr = [1,2,3,4,5]
>> (0..2).each |i| puts arr[i]
1
2
3
尽管开起来比较丑,我敢打赌当数组元素数量增大而且需要很长时间来拷贝时第二种方式更加有效。
#each与#map/#collect
除了#each之外,你可能还经常碰到#map。#map类似于#each,只是它是在each块调用的结果上生成数组。这很有用,因为#each返回的仅仅是调用each的哪个数组。
>> [1,2,3].each |i| i + 1
=> [1,2,3]
>> [1,2,3].map |i| i + 1
=> [2,3,4]
如果你只是对每个元素调用同一方法,那么你可以使用方便的快捷简写:
>> [1,2,3].map(&:to_f)
=> [1.0, 2.0, 3.0]
这个等同于:
>> [1,2,3].map |i| i.to_f
=> [1.0, 2.0, 3.0]
注意: #map不会更改原始的数组值。它仅仅返回的是基于each块调用结果而生成的数组。如果你想在原始数组上反映出这种更改的话,请使用#map!。
>> numbers = [1,2,3]
>> numbers.map(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1, 2, 3]
>> numbers.map!(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1.0, 2.0, 3.0]
你还可能注意到Ruby代码里的#collect。它与#map相同,因此它们两个是可互换的。
>> letters = ["a", "b", "c"]
>> letters.map(&:capitalize)
=> ["A", "B", "C"]
>> letters.collect(&:capitalize)
=> ["A", "B", "C"]
传统的迭代
Ruby提供了传统的"for"风格的迭代。虽然这种风格看起来更清晰,而且对哪些从其他语言转过来的新手来收更熟悉,但是这种风格不符合语言习惯,因为它不是面向对象的,同时也不接受块参数。
>> animals = ["cat", "dog", "bird", "chuck testa"]
>> for animal in animals
>> puts animal.upcase
>> end
CAT
DOG
BIRD
CHUCK TESTA
如果“不符合语言习惯”还不足以阻止你的话,可以看看下面这个由Nikals B.在stackoverflow上展示的异常结果:
>> results = []
>> (1..3).each |i| results << lambda i
>> results.map(&:call)
=> [1, 2, 3]
>> results = []
>> for i in 1..3
>> results << lambda i
>> end
>> result.map(&:call)
=> [3, 3, 3]
另外,“for"循环所创建的“临时”变量根本就不是临时的。那上面for循环中animals中的anmial变量怎么是正确的呢? 不,不正确。
>> animal
=> "chuck testa"
结论:Ruby的for最适合于让你疑惑的眼珠转个不停,或者最适合于让哪些参加会议的格外关心。