深入探索
Hashes, Arrays, Ranges 和 Sets 都包含(include)了一个名为 Enumerable 的 Ruby 模块(module)。模块是一种代码库(我将在第 12 章中更多地讨论模块)。在第 4 章中,我使用了 Comparable 模块为数组添加比较方法,例如 <
和 >
。你可能还记得我是通过继承 Array 类并将 Comparable 模块 “including” 到子类中来完成此操作:
class Array2 < Array
include Comparable
end
Enumerable 模块
Enumerable 模块已经被包含进了 Ruby 的 Array 类中,它提供了很多有用的方法,例如 include?
方法会在数组中找到一个特定的值时返回 true,min
方法则会返回最小的元素值,max
方法返回最大的元素值,collect
方法会创建一个由块(block)返回的值组成的新数组。
arr = [1,2,3,4,5]
y = arr.collect{ |i| i } #=> y = [1, 2, 3, 4]
z = arr.collect{ |i| i * i } #=> z = [1, 4, 9, 16, 25]
arr.include?( 3 ) #=> true
arr.include?( 6 ) #=> false
arr.min #=> 1
arr.max #=> 5
只要其它集合类包含 Enumerable 模块,就可以使用这些相同的方法。Hash 就是一个这样的类。但请记住,Hash 中的元素索引是没有顺序的,因此当你使用 min
和 max
方法时,将根据其数值返回最小和最大元素值 - 当元素值为字符串时,其数值由键(key)中字符的 ASCII 码确定。
自定义比较
但是我们假设你更喜欢 min
和 max
根据一些其它标准(比如字符串的长度)返回元素?最简单的方法是在块(block)内定义比较的本质。这与我在第 4 章中定义的排序块类似。你可能还记得我们通过将块(block)传递给 sort
方法来对 Hash(此处为变量 h
)进行排序,如下所示:
h.sort{ |a,b| a.to_s <=> b.to_s }
两个参数 a
和 b
表示来自 Hash 的两个元素,使用 <=>
比较方法进行比较。我们可以类似地将块(block)传递给 max
和 min
方法:
h.min { |a,b| a[0].length <=> b[0].length }
h.max { |a,b| a[0].length <=> b[0].length }
当 Hash 将元素传递给块时,它会以包含键值对(key-value)的数组形式传递。所以,如何一个 Hash 包含这样的元素…
{"one"=>"for sorrow", "two"=>"for joy"}
…两个块参数,a
和 b
将会被初始化为两个数组:
a = ["one", "for sorrow"]
b = ["two", "for joy"]
这解释了为什么我在为 max
和 min
方法定义的自定义比较中特意比较的是两个块参数中位于索引 0 处的首个元素:
a[0].length <=> b[0].length
这确保了比较是基于哈希中的键(keys)的。
如果你要比较值(values),而不是键(keys),只需要将数组的索引设置为 1:
p( h.min {|a,b| a[1].length <=> b[1].length } )
p( h.max {|a,b| a[1].length <=> b[1].length } )
当然,你可以在块中定义其他类型的自定义比较。例如,假设你希望字符串 ‘one’,’two’,’three’ 等按照我们说它们的顺序进行执行。这样做的一种方法是创建一个有序的字符串数组:
str_arr=['one','two','three','four','five','six','seven']
现在,如果一个 Hash,h
包含这些字符串作为键(key),则块可以使用 str_array
作为键的引用以确定最小值和最大值:
h.min { |a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}
#=> ["one", "for sorrow"]
h.max { |a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}
#=> ["seven", "for a secret never to be told"]
上面所有的示例都使用了 Array 和 Hash 类的 min
和 max
方法。请记住,是 Enumerable 模块给这些类提供了这些方法。
在某些情况下,能够将诸如 max
,min
和 collect
之类的 Enumerable 方法应用于不是从现有的实现这些方法的类(例如 Array)中派生出来的类中是有用的。你可以在你的类中包含 Enumerable 模块,然后编写一个名为 each
的迭代器方法:
class MyCollection
include Enumerable
def initialize( someItems )
@items = someItems
end
def each
@items.each { |i|
yield( i )
}
end
end
在这里,你可以使用数组初始化一个 MyCollection 对象,该数组将存储在实例变量 @items
中。当你调用 Enumerable 模块提供的方法之一(例如 min
,max
或 collect
)时,这将“在幕后”(behind the scenes)调用 each
方法,以便一次获取一个数据。
things = MyCollection.new(['x','yz','defgh','ij','klmno'])
p( things.min ) #=> "defgh"
p( things.max ) #=> "yz"
p( things.collect{ |i| i.upcase } )
#=> ["X", "YZ", "DEFGH", "IJ", "KLMNO"]
你可以类似地使用 MyCollection
类来处理数组,例如 Hashes 的键(keys)或值(values)。目前,min
和 max
方法采用基于数值执行比较的默认行为,因此基于字符的ASCII 值,’xy’ 将被认为比 ‘abcd’’更大’。如果你想执行一些其它类型的比较 - 例如,通过字符串长度来比较,以便 ‘abcd’ 被认为大于 ‘xz’ - 你可以覆盖 min
和 max
方法:
def min
@items.to_a.min { |a,b| a.length <=> b.length }
end
def max
@items.to_a.max { |a,b| a.length <=> b.length }
end
Each and Yield…
那么,当 Enumerable 模块中的方法调用你编写的each
方法时,真正发生了什么?事实证明,Enumerable 方法(min
,max
,collect
等)给 each
方法传递了一个代码块(block)。这段代码期望一次接收一个数据(即来自某种集合的每个元素)。你的 each
方法以块参数的形式为其提供该项,例如此处的参数 i
: def each @items.each{ |i| yield( i ) } end关键字 yield
是一个特殊的 Ruby 魔术,它告诉代码运行传递给 each
方法的块 - 也就是说,运行 Enumerator 模块的 min
,max
或 collect
方法传递的代码块。这意味着这些方法的代码块可以应用于各种不同类型的集合。你所要做的就是,i)在你的类中包含 Enumerable 模块;ii)编写 each
方法,确定 Enumerable 方法将使用哪些值。