深入探索

Hashes, Arrays, Ranges 和 Sets 都包含(include)了一个名为 Enumerable 的 Ruby 模块(module)。模块是一种代码库(我将在第 12 章中更多地讨论模块)。在第 4 章中,我使用了 Comparable 模块为数组添加比较方法,例如 <>。你可能还记得我是通过继承 Array 类并将 Comparable 模块 “including” 到子类中来完成此操作:

  1. class Array2 < Array
  2. include Comparable
  3. end

Enumerable 模块

enum.rb

Enumerable 模块已经被包含进了 Ruby 的 Array 类中,它提供了很多有用的方法,例如 include? 方法会在数组中找到一个特定的值时返回 true,min 方法则会返回最小的元素值,max 方法返回最大的元素值,collect 方法会创建一个由块(block)返回的值组成的新数组。

  1. arr = [1,2,3,4,5]
  2. y = arr.collect{ |i| i } #=> y = [1, 2, 3, 4]
  3. z = arr.collect{ |i| i * i } #=> z = [1, 4, 9, 16, 25]
  4. arr.include?( 3 ) #=> true
  5. arr.include?( 6 ) #=> false
  6. arr.min #=> 1
  7. arr.max #=> 5
enum2.rb

只要其它集合类包含 Enumerable 模块,就可以使用这些相同的方法。Hash 就是一个这样的类。但请记住,Hash 中的元素索引是没有顺序的,因此当你使用 minmax 方法时,将根据其数值返回最小和最大元素值 - 当元素值为字符串时,其数值由键(key)中字符的 ASCII 码确定。

自定义比较

但是我们假设你更喜欢 minmax 根据一些其它标准(比如字符串的长度)返回元素?最简单的方法是在块(block)内定义比较的本质。这与我在第 4 章中定义的排序块类似。你可能还记得我们通过将块(block)传递给 sort 方法来对 Hash(此处为变量 h)进行排序,如下所示:

  1. h.sort{ |a,b| a.to_s <=> b.to_s }

两个参数 ab 表示来自 Hash 的两个元素,使用 <=> 比较方法进行比较。我们可以类似地将块(block)传递给 maxmin 方法:

  1. h.min { |a,b| a[0].length <=> b[0].length }
  2. h.max { |a,b| a[0].length <=> b[0].length }

当 Hash 将元素传递给块时,它会以包含键值对(key-value)的数组形式传递。所以,如何一个 Hash 包含这样的元素…

  1. {"one"=>"for sorrow", "two"=>"for joy"}

…两个块参数,ab 将会被初始化为两个数组:

  1. a = ["one", "for sorrow"]
  2. b = ["two", "for joy"]

这解释了为什么我在为 maxmin 方法定义的自定义比较中特意比较的是两个块参数中位于索引 0 处的首个元素:

  1. a[0].length <=> b[0].length

这确保了比较是基于哈希中的(keys)的。

如果你要比较(values),而不是键(keys),只需要将数组的索引设置为 1:

enum3.rb
  1. p( h.min {|a,b| a[1].length <=> b[1].length } )
  2. p( h.max {|a,b| a[1].length <=> b[1].length } )

当然,你可以在块中定义其他类型的自定义比较。例如,假设你希望字符串 ‘one’,’two’,’three’ 等按照我们说它们的顺序进行执行。这样做的一种方法是创建一个有序的字符串数组:

  1. str_arr=['one','two','three','four','five','six','seven']

现在,如果一个 Hash,h 包含这些字符串作为键(key),则块可以使用 str_array 作为键的引用以确定最小值和最大值:

  1. h.min { |a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}
  2. #=> ["one", "for sorrow"]
  3. h.max { |a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}
  4. #=> ["seven", "for a secret never to be told"]

上面所有的示例都使用了 Array 和 Hash 类的 minmax 方法。请记住,是 Enumerable 模块给这些类提供了这些方法。

在某些情况下,能够将诸如 maxmincollect 之类的 Enumerable 方法应用于不是从现有的实现这些方法的类(例如 Array)中派生出来的类中是有用的。你可以在你的类中包含 Enumerable 模块,然后编写一个名为 each 的迭代器方法:

include_enum1.rb
  1. class MyCollection
  2. include Enumerable
  3. def initialize( someItems )
  4. @items = someItems
  5. end
  6. def each
  7. @items.each { |i|
  8. yield( i )
  9. }
  10. end
  11. end

在这里,你可以使用数组初始化一个 MyCollection 对象,该数组将存储在实例变量 @items 中。当你调用 Enumerable 模块提供的方法之一(例如 minmaxcollect)时,这将“在幕后”(behind the scenes)调用 each 方法,以便一次获取一个数据。

  1. things = MyCollection.new(['x','yz','defgh','ij','klmno'])
  2. p( things.min ) #=> "defgh"
  3. p( things.max ) #=> "yz"
  4. p( things.collect{ |i| i.upcase } )
  5. #=> ["X", "YZ", "DEFGH", "IJ", "KLMNO"]
include_enum2.rb

你可以类似地使用 MyCollection 类来处理数组,例如 Hashes 的键(keys)或值(values)。目前,minmax 方法采用基于数值执行比较的默认行为,因此基于字符的ASCII 值,’xy’ 将被认为比 ‘abcd’’更大’。如果你想执行一些其它类型的比较 - 例如,通过字符串长度来比较,以便 ‘abcd’ 被认为大于 ‘xz’ - 你可以覆盖 minmax方法:

include_enum3.rb
  1. def min
  2. @items.to_a.min { |a,b| a.length <=> b.length }
  3. end
  4. def max
  5. @items.to_a.max { |a,b| a.length <=> b.length }
  6. end

Each and Yield…

那么,当 Enumerable 模块中的方法调用你编写的 each 方法时,真正发生了什么?事实证明,Enumerable 方法(minmaxcollect 等)给 each 方法传递了一个代码块(block)。这段代码期望一次接收一个数据(即来自某种集合的每个元素)。你的 each 方法以块参数的形式为其提供该项,例如此处的参数 i: def each @items.each{ |i| yield( i ) } end关键字 yield 是一个特殊的 Ruby 魔术,它告诉代码运行传递给 each 方法的块 - 也就是说,运行 Enumerator 模块的 minmaxcollect 方法传递的代码块。这意味着这些方法的代码块可以应用于各种不同类型的集合。你所要做的就是,i)在你的类中包含 Enumerable 模块;ii)编写 each 方法,确定 Enumerable 方法将使用哪些值。