一聚教程网:一个值得你收藏的教程网站

热门教程

Ruby 枚举的使用详解

时间:2022-06-25 02:13:24 编辑:袖梨 来源:一聚教程网

通过  each 获得枚举能力

想要枚举的 Class 就得有一个 each 方法,它的工作就是 yield 项目给代码块,每次一个。

each 做的事在不同的类上可能不太一样,比如在数组上,each 会 yield 第一个项目,然后是第二个,第三个 ...  在 hash 里,yield 的是 key/value 作为两个元素的数组。在 file 处理上,每次会 yield 一行内容。range 迭代会先看一下有没有可能迭代,然后假装它是一个数组。在自己的类里定义了 each 方法,它的意思是你自己定义的,只要它 yield 点东西就行。

先写个类,理解一下 Enumerable 是怎么回事。Rainbow 彩虹,每次 yield 一种颜色:

class Rainbow
  include Enumerable

  def each
    yield 'red'
    yield 'orange'
    yield 'yellow'
    yield 'green'
    yield 'blue'
    yield 'indigo'
    yield 'violet'
  end
end
用一下 each 方法:

r = Rainbow.new
r.each do |color|
  puts "下个颜色:#{color}"
end
执行的结果:

下个颜色:red
下个颜色:orange
下个颜色:yellow
下个颜色:green
下个颜色:blue
下个颜色:indigo
下个颜色:violet
Rainbow 里混合了 Enumerable 模块,这样 Rainbow 的实例就自动会拥有一大堆基于 each 创建的方法。

find,代码块里返回 true 就会返回第一个元素。比如找到第一个 y 开头的颜色:

y_color = r.find {|color| color.start_with?('y')}
puts "第一个用 y 开头的颜色是:#{y_color}"
结果是:

第一个用 y 开头的颜色是:yellow
find 会调用 each,each  yield 项目,find 在代码块里每次测试一个项目。当 each yield yellow 的时候,find 的代码块通过了测试,y_color 的值就会是 yellow 。

看看 Enumerable 里都有啥:

>> Enumerable.instance_methods(false).sort
=> [:all?, :any?, :chunk, :chunk_while, :collect, :collect_concat, :count, :cycle, :detect, :drop, :drop_while, :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object, :entries, :find, :find_all, :find_index, :first, :flat_map, :grep, :grep_v, :group_by, :include?, :inject, :lazy, :map, :max, :max_by, :member?, :min, :min_by, :minmax, :minmax_by, :none?, :one?, :partition, :reduce, :reject, :reverse_each, :select, :slice_after, :slice_before, :slice_when, :sort, :sort_by, :take, :take_while, :to_a, :to_h, :to_set, :zip]
枚举布尔查询

有些枚举方法会根据一个或多个元素匹配特定的标准返回 true 或 false。

试一下这些:

>> provinces = ["山东", "山西", "黑龙江"]
=> ["山东", "山西", "黑龙江"]
>> provinces.include?("山东")
=> true
>> provinces.all? {|province| province =~ /山/}
=> false
>> provinces.any? {|province| province =~ /山/}
=> true
>> provinces.one? {|province| province =~ /黑/}
=> true
>> provinces.none? {|province| province =~ /河/}
=> true
继续实验:

>> provinces = {'山东' => '鲁', '山西' => '晋', '黑龙江' => '黑'}
=> {"山东"=>"鲁", "山西"=>"晋", "黑龙江"=>"黑"}
>> provinces.include?("山东")
=> true
>> provinces.all? {|province, abbr| province =~ /山/}
=> false
>> provinces.one? {|province, addr| province =~ /黑/}
=> true
也可以换个方法:

>> provinces.keys.all? {|province, abbr| province =~ /山/}
=> false
hash

each 迭代一个 hash ,hash 会被 yield 到你的代码块里,每次一个 key/value 对。每对是一个两个元素的数组。

range

>> r = Range.new(1, 10)
=> 1..10
>> r.one? {|n| n == 5}
=> true
>> r.none? {|n| n % 2 == 0}
=> false
>> r = Range.new(1.0, 10.0)
=> 1.0..10.0
>> r.one {|n| n == 5}
NoMethodError: undefined method `one' for 1.0..10.0:Range
Did you mean? one?
 from (irb):402
 from /usr/local/bin/irb:11:in `

'
>> r = Range.new(1, 10.3)
=> 1..10.3
>> r.any? {|n| n > 5}
=> true
枚举搜索与选择

基于一个或多个标准去过滤一个集合的对象。基于一个或多个标准在一个集合的对象里选择项目。我们看一下过滤与搜索的方法,它们都是迭代器,它们都期待你提供一个代码块。在代码块里定义选择标准。

find 第一个匹配

find 或 detect。例:一个整数的数组里找到第一个大于 5 的数字:

>> [1,2,3,4,5,6,7,8,9,10].find {|n| n > 5 } => 6
find 会迭代整个数组,每回都会 yield 一个元素给代码块。方法返回 true,被 yield 的元素就赢了,然后停止迭代。

元素不能通过代码块的测试,方法会返回 nil 。

find_all 与 reject

find_all 又名 select 。返回的新集合里包含匹配的所有的项目。没找到东西会返回空白的集合对象。

>> a = [1,2,3,4,5,6,7,8,9,10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>> a.find_all {|item| item >5}
=> [6, 7, 8, 9, 10]
>> a.select {|item| item > 100}
=> []
reject

>> a.reject {|item| item > 5}
=> [1, 2, 3, 4, 5]
grep

Enumerable#grep 方法,基于 === 操作符来选择。

>> colors = %w{ red orange yellow green blue indigo violet }
=> ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
>> colors.grep(/o/)
=> ["orange", "yellow", "indigo", "violet"]
再试试:

>> miscellany = [75, 'hello', 10..20, 'goodbye']
=> [75, "hello", 10..20, "goodbye"]
>> miscellany.grep(String)
=> ["hello", "goodbye"]
>> miscellany.grep(50..100)
=> [75]
enumerable.grep(expression) 的功能相当于:

enumerable.select {|element| expression === element }
为 grep 提供一个代码块:

>> colors = %w{ red orange yellow green blue indigo violet }
=> ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
>> colors.grep(/o/) {|color| color.capitalize }
=> ["Orange", "Yellow", "Indigo", "Violet"]
语法:

enumerable.grep(expression) {|item| ... }
相当于:

enumerable.select {|item| expression === item}.map {|item| ... }
group_by

分组,试一下:

>> colors = %w{ red orange yellow green blue indigo violet }
=> ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
>> colors.group_by {|color| color.size}
=> {3=>["red"], 6=>["orange", "yellow", "indigo", "violet"], 5=>["green"], 4=>["blue"]}
partition

>> colors.partition {|color| color.size > 5}
=> [["orange", "yellow", "indigo", "violet"], ["red", "green", "blue"]]
一个 Person 类,每个都有年龄,里面有个 teenager? 实例方法,如果人的年龄在 13 - 19 之间就返回 true。

class Person
  attr_accessor :age
  def initialize(options)
    self.age = options[:age]
  end
  def teenager?
    (13..19) === age
  end
end
生成一组人:

>> people = 10.step(25,3).map {|i| Person.new(:age => i)}
=> [#, #, #, #, #, #]
teens:

>> teens = people.partition {|person| person.teenager?}
=> [[#, #, #], [#, #, #]]
多少青年人?多少不是

>> puts "#{teens[0].size} teens; #{teens[1].size} non-teens"
3 teens; 3 non-teens
=> nil
迭代操作

first

>> [1,2,3,4].first
=> 1
>> (1..10).first
=> 1
>> {1 => "one", 2 => "two"}.first
=> [1, "one"]
first 方法返回来的对象,跟你用 each 第一次 yield 的东西是一样的。

>> hash = {3 => "three", 1 => "one", 2 => "two"}
=> {3=>"three", 1=>"one", 2=>"two"}
>> hash.first
=> [3, "three"]
>> hash[3] = "trois"
=> "trois"
>> hash.first
=> [3, "trois"]
take 与 drop 方法

2016-09-13 07:33 **

take 取,drop 扔。

>> provinces = %w{ 黑 吉 辽 京 津 沪}
=> ["黑", "吉", "辽", "京", "津", "沪"]
>> provinces.take(2)
=> ["黑", "吉"]
>> provinces.drop(2)
=> ["辽", "京", "津", "沪"]
take_while 与 drop_while,可以提供个代码块。

>> provinces = %w{ 山东 山西 河南 河北}
=> ["山东", "山西", "河南", "河北"]
>> provinces.take_while {|p| /山/.match(p)}
=> ["山东", "山西"]
>> provinces.drop_while {|p| /河/.match(p)}
=> ["山东", "山西", "河南", "河北"]
min 与 max 方法

>> [1,2,3].max
=> 3
>> [1,2,3].min
=> 1
最小与最大是用 <=> 判定的。

min_by 与 max_by ,给它们提供个代码块。

>> ["hi", "hello"].min_by {|w| w.size}
=> "hi"
>> ["hi", "hello"].max_by {|w| w.size}
=> "hello"
minmax 与 minmax_by ,最小最大一样一个。

>> [1,2,3].minmax
=> [1, 3]
hash,用 key 来判定,想用 value 判定,可以用 *_by 方法提供个代码块。

>> n = { 1 => "one", 2 => "two", 3 => "three" }
=> {1=>"one", 2=>"two", 3=>"three"}
>> n.min
=> [1, "one"]
>> n.max
=> [3, "three"]
>> n.max_by {|k, v| v}
=> [2, "two"]
>> n.min_by {|k, v| v}
=> [1, "one"]
each 的亲戚

reverse_each

>> [1,2,3].reverse_each {|e| puts e * 10 }
30
20
10
=> [1, 2, 3]
each_with_index

>> provinces = ["黑", "吉", "辽"]
=> ["黑", "吉", "辽"]
>> provinces.each_with_index do |p, i|
?> puts "#{i+1}. #{p}"
>> end
1. 黑
2. 吉
3. 辽
=> ["黑", "吉", "辽"]
each_index

>> %w{a b c}.each_index {|i| puts i}
0
1
2
=> ["a", "b", "c"]
hash

>> letters = {"a" => "ay", "b" => "bee", "c" => "see"}
=> {"a"=>"ay", "b"=>"bee", "c"=>"see"}
>> letters.each_with_index {|(k,v), i| puts i}
0
1
2
=> {"a"=>"ay", "b"=>"bee", "c"=>"see"}
>> letters.each_index {|(k,v), i| puts i}
NoMethodError: undefined method `each_index' for {"a"=>"ay", "b"=>"bee", "c"=>"see"}:Hash
Did you mean? each_with_index
 from (irb):31
 from /usr/local/bin/irb:11:in `

'
each_slice 与 each_cons

>> array = [1,2,3,4,5,6,7,8,9,10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>> array.each_slice(3) {|slice| p slice}
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
=> nil
>> array.each_cons(3) {|cons| p cons}
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 10]
=> nil
cycle

class PlayingCard
  SUITS = %w{clubs diamonds hearts spades}
  RANKS = %w{ 2 3 4 5 6 7 8 9 10 J Q K A}
  class Deck
    attr_reader :cards
    def initialize(n=1)
      @cards = []
      SUITS.cycle(n) do |s|
        RANKS.cycle(n) do |s|
          @cards << "#{r} of #{s}"
        end
      end
    end
  end
end
例:

deck = PlayingCard::Deck.new(2)
状态不好,回头再看这个方法。
inject

inject,reduce。

一个数组求和,acc 表示 accumulator :

>> [1,2,3].inject(0) {|acc,n| acc + n}
=> 6
看看发生了什么:

>> [1,2,3].inject do |acc,n|
?>   puts "adding #{acc} and #{n}...#{acc+n}"
>> acc + n
>> end
adding 1 and 2...3
adding 3 and 3...6
=> 6
map

map(collect)。永远返回的是数组,数组的尺寸跟原始数组的尺寸一样。

>> names = %w{ David Yukihiro Chad Amy }
=> ["David", "Yukihiro", "Chad", "Amy"]
>> names.map {|name| name.upcase }
=> ["DAVID", "YUKIHIRO", "CHAD", "AMY"]
map 返回的值

map 返回新的对象:

result = array.map {|x| # code here...}
做个实验:

>> array = [1,2,3]
=> [1, 2, 3]
>> result = array.map {|n| puts n * 100}
100
200
300
=> [nil, nil, nil]
result 是 [nil, nil, nil],因为 puts 返回的值是 nil 。

in-place mapping

map! 是在 Array 里定义的,它不在 Enumerable 里。

实验:

>> names = %w{ David Yukihiro Chad Amy }
=> ["David", "Yukihiro", "Chad", "Amy"]
>> names.map!(&:upcase)
=> ["DAVID", "YUKIHIRO", "CHAD", "AMY"]
枚举字符串

用 each_bype 迭代字节:

>> str = "abcde"
=> "abcde"
>> str.each_byte {|b| p b}
97
98
99
100
101
=> "abcde"
想要的不是字节代码,而是字符,可以使用 each_char:

>> str = "abcde"
=> "abcde"
>> str.each_char {|b| p b}
"a"
"b"
"c"
"d"
"e"
=> "abcde"
code point:

>> str = "100u20ac"
=> "100€"
>> str.each_codepoint {|cp| p cp }
49
48
48
8364
=> "100€"
byte by byte:

>> str.each_byte {|b| p b}
49
48
48
226
130
172
=> "100€"
line by line,用 each_line:

>> str = "this stringnhas threenlines"
=> "this stringnhas threenlines"
>> str.each_line {|l| puts "next line: #{l}"}
next line: this string
next line: has three
next line: lines
=> "this stringnhas threenlines"
根据全局变量 $/ 的值分行,你可以改变这个变量的值:

>> str = "i!love!you"
=> "i!love!you"
>> $/ = "!"
=> "!"
>> str.each_line {|l| puts "next line: #{l}"}
next line: i!
next line: love!
next line: you
=> "i!love!you"
数组的 bytes,chars,codepoints,lines:

>> str = "hello"
=> "hello"
>> p str.bytes
[104, 101, 108, 108, 111]
=> [104, 101, 108, 108, 111]
>> p str.chars
["h", "e", "l", "l", "o"]
=> ["h", "e", "l", "l", "o"]
>> p str.codepoints
[104, 101, 108, 108, 111]
=> [104, 101, 108, 108, 111]
>> p str.lines
["hello"]
=> ["hello"]
存储枚举

你有个类,你想按顺序安排多个实例,你需要:

在类里创建比较方法(<=>)。
把多个实例放到一个容器里,比如一个数组。
对容器排序。
排序的能力是 Enumerable 给的,在你的类里不需要混合这个模块,你可以把你的对象放到一个混合了 Enumerable 的容器里。这个容器对象是可以枚举的,你可以使用 sort 与 sort_by 对集合进行排序。

对一个数字数组排序:

>> [3,2,5,1,4].sort
=> [1, 2, 3, 4, 5]
对数字排序很容易,如果你想对一组画作排序呢?

>> [pa1, pa2, pa3, pa4, pa5].sort
可以在自己的类里定义 <=> 方法。假设每个画作都有一个 price 属性,表示它的价格:

def <=>(other_painting)
  self.price <=> other_painting.price
end
现在对一组画作排序的话,就会根据它的价格来排序。

Comparable 模块

在类里如果定义了 <=>,类的实例就可以放到数组或其它可枚举的对象里排序。
如果不定义 <=>,你也可以排序对象,不过你需要把它们放到一个数组里,并且提供一个代码块,告诉数组你打算按什么东西来排序。
如果你在类里定义了 <=>,也混合了 Comparable 。你就获得了在一个数组里的排序能力,并且你的对象可以执行所有的比较操作。
在代码块里定义排序标准

我们定义了 Painting#<=> ,排画作价格排序,现在你想强制使用按年代排序,这样做:

year_sort = [pa1, pa2, pa3, pa4, pa5].sort do |a,b|
  a.year <=> b.year
end
两种东西不知道怎么比较彼此,比如整数与字符串,如果你 "2" <=> 4 ,会报错,你可以先这样做:

>> ["2",1,5,"3",4,"6"].sort {|a,b| a.to_i <=> b.to_i }
=> [1, "2", "3", 4, 5, "6"]
sort_by

sort 与 sort_by 是 Enumerable 的实例方法。区别是,sort_by 必须给它提供代码块,你只需要告诉它怎么对待集合里的项目就行了:

>> ["2",1,5,"3",4,"6"].sort_by {|a| a.to_i}
=> [1, "2", "3", 4, 5, "6"]
枚举器与下一维度的枚举

状态好些了,我不适合起早 :)
枚举器(enumerator)与迭代器(iterator)很像,但又不是一回事。迭代器是个方法,它 yield 一个或多个值给代码块。枚举器是个对象,不是方法。本质上枚举器是一个简单的可枚举的对象。它有 each 方法,它用了 Enumerable 模块,定义了常用的方法,比如:select,inject,map 等等。

枚举器不是一个容器对象,它的 each 功能不是天生就会的,你得告诉它怎么样 each,这样枚举器可以自己整明白怎么去 map,find,take,drop 等等。

创建枚举器的两种方法

调用 Enumerator.new,给它一个代码块,里面包含 each 逻辑。
把枚举器附加到其它对象的迭代器上。
用代码块创建枚举器

e = Enumerator.new do |y|
  y << 1
  y << 2
  y << 3
end
y 是一个 yielder,它是 Enumerator::Yielder 的一个实例,会自动传递到你的代码块里。上面的例子里,枚举器调用了 each 的时候,先 yield 1,然后是 2,然后是 3 。<< 方法告诉  yielder 要 yield 的是什么东西。

用一下 e:

>> e.to_a
=> [1, 2, 3]
>> e.map {|x| x * 10}
=> [10, 20, 30]
>> e.select {|x| x > 1}
=> [2, 3]
>> e.take(2)
=> [1, 2]
重写一下 e:

e = Enumerator.new do |y|
  (1..3).each {|i| y << i }
end
e 的行为跟之前是一样的。

不要直接在代码块里 yield ,你不能:

e = Enumerator.new do
  yield 1
  yield 2
  yield 3
end
追踪一下方法的执行:

e = Enumerator.new do |y|
  puts "Starting up the block!"
  (1..3).each {|i| y << i }
  puts "Exiting the block!"
end
p e.to_a
p e.select {|x| x > 2 }
输出:

Starting up the block!
Exiting the block!
[1, 2, 3]
Starting up the block!
Exiting the block!
[3]
再试一下:

a = [1,2,3,4,5]
e = Enumerator.new do |y|
  total = 0
  until a.empty?
    total += a.pop
    y << total
  end
end
用一下:

>> e.take(2)
=> [5, 9]
>> a
=> [1, 2, 3]
>> e.to_a
=> [3, 5, 6]
>> a
=> []
把枚举器附加到其它对象

把枚举器附加到其它的对象上的一个迭代器里。这个迭代器一般是 each ,不过任何可以 yield 一个或多个值的方法都可以。这样就会给了枚举器可以迭代的主要功能。当它需要 yied 点东西的时候,它会触发它附加到的那个对象去 yield。

使用这种方法创建枚举器,可以在对象上调用 enum_for(又名:to_enum)方法,这个方法的第一个参数可以设置一下枚举器的 each 方法要附加到的那个方法的名字。默认这个参数的值是 :each,你也可以换成其它的方法,比如:

names = %w{ Matt Damon Scarlett Johansson }
e = names.enum_for(:select)
方法的参数设置成了 :select,意思就是枚举器绑定到了 names 这个数组的 select 这个方法上了。也就是枚举器的 each 方法用起来跟数组的 select 方法一样:

>> e.each {|n| n.include?("s")}
=> ["Johansson"]
enum_for 的其它参数:

>> names = %w{ Matt Damon Scarlett Johansson }
=> ["Matt", "Damon", "Scarlett", "Johansson"]
>> e = names.enum_for(:inject, "Names: ")
=> #
>> e.each {|string, name| string << "#{name}..."}
=> "Names: Matt...Damon...Scarlett...Johansson..."
两种形式,效果一样:

Enumerator.new(obj, method_name, arg1, arg2...)
obj.enum_for(method_name, arg1, arg2...)
隐式枚举器

迭代器是方法,它可以给代码块提供一个或多个值。调用迭代器的时候如果没给它提供代码块的话,大部分迭代器会返回一个枚举器。

>> str = 'hello'
=> "hello"
>> str.each_byte {|b| puts b}
104
101
108
108
111
=> "hello"
如果这样:

>> str.each_byte
=> #
相当于是:

>> str.enum_for(:each_byte)
枚举器的语义与使用

调用 each 的时候会发生什么。用枚举器更好的控制迭代。

使用枚举器的 each 方法

枚举器的 each 方法跟另一个对象的方法勾搭到一块儿了,这个方法很可能是 each 以外的方法。如果你直接去使用它,它会像其它的方法,包括它返回的值。

这可能会产生一些看起来很奇怪的结果,调用 each 返回过滤过的,排序过的,或者映射过的集合。看个例子:

>> array = %w{ cat dog rabbit }
=> ["cat", "dog", "rabbit"]
>> e = array.map
=> #
>> e.each {|animal| animal.capitalize}
=> ["Cat", "Dog", "Rabbit"]
枚举器跟那个数组不是同一个对象。关于 each 是什么意思它有它自己的想法。然而,把一个枚举器连接到一个数组的 map 方法,使用 each ,它返回的值是 mapping 过的一个数组。通常来说,each 迭代一个数组,主要存在是为了它的副作用还有会返回它的接收者(一个数组)。

But an enumerator’s each serves as a kind ofconduit to the method from which it pulls its values and behaves the same way in the matter of return value.
但是呢,一个枚举器的 each 的功能是作为一种管道到一个方法上,拉取它的值与行为,还有返回值。

未覆盖现象

要是一个对象定义了 each 方法也包含了 Enumerable,它的实例就会自动得到 map,select,inject 还有其它的 Enumerable 方法。所有这些方法都是根据 each 定义的。

但是有的时候,类已经使用了自己的方法覆盖掉了在 Enumerable 里的方法。比如说 Hash#select 。标准的在 Enumerable 里的 select 方法返回的永远是数组,不过在 hash 里的 select 会返回一个 hash :

>> h = {"cat" => "猫", "dog" => "狗", "cow" => "牛"}
=> {"cat"=>"猫", "dog"=>"狗", "cow"=>"牛"}
>> h.select {|k,v| k =~ /c/}
=> {"cat"=>"猫", "cow"=>"牛"}
如果我们把枚举器挂到 select 方法上,它会给我们一个跟 select 一样的 each 方法:

>> e = h.enum_for(:select)
=> #"猫", "dog"=>"狗", "cow"=>"牛"}:select>
>> e.each {|k,v| k =~ /c/}
=> {"cat"=>"猫", "cow"=>"牛"}
如果我们把枚举器挂到 hash 的 each 方法上呢?

>> e = h.to_enum
=> #"猫", "dog"=>"狗", "cow"=>"牛"}:each>
调用 Hash#each ,带着代码块,返回 hash。枚举器的 each 也一样,因为它只是 hash 的 each 的一个前端。下面的代码块是空白的,因为只关心它的返回值:

>> h.each { }
=> {"cat"=>"猫", "dog"=>"狗", "cow"=>"牛"}
>> e.each { }
=> {"cat"=>"猫", "dog"=>"狗", "cow"=>"牛"}
我们要是用这个 each 的执行一个 select 控制会怎样?

>> e.select {|k,v| k =~ /c/}
=> [["cat", "猫"], ["cow", "牛"]]
我们会得到一个 array,而不是一个 hash 。如果 e.each 挂到 h.each 上,为何 e.select 返回的值没有跟 h.select 挂钩呢?问题的关键是上面调用的 select 其实调用的是枚举器上的 select 方法,并不是 hash 上的 select 。枚举器上的 select 方法是基于枚举器的 each 方法创建的。这个 select 是 Enumerable#select ,它返回的永远是一个 array。Hash#select 返回的是 hash 。

枚举器给 hash 添加枚举的能力,即使 hash 已经可以枚举了。同时未覆盖 Enumerable#select,select 是枚举器的 Enumerable#select,hash 的 select 并不是。

重要是你要记住,枚举器跟它吸入迭代对象的那个集合并不是同一个对象。通过枚举器访问集合会保护集合对象不被修改。

用枚举器保护对象

假设一个方法需要使用一个 array 作为它的参数。

def give_me_an_array(array)
如果你给这个方法一个 array 对象,方法可以修改这个对象:

array << "new element"
如果你想保护这个原始的 array 对象,你可以复制一份,再把这个复制品给这个方法。或者你也可以给它传递一个枚举器:

give_me_an_array(array.to_enum)
枚举器允许迭代数组,但它不接收修改,比如你在它上面调用 << 的时候就会报错。也就是枚举器可以作为一种集合对象的保护机制,它可以允许迭代,检验集合里的元素,但是不让你执行破坏性的操作。

枚举器可以更好的控制迭代

先做个实验:

>> pets = %w{ dog cat }
=> ["dog", "cat"]
>> e = pets.to_enum
=> #
>> puts e.next
dog
=> nil
>> puts e.next
cat
=> nil
>> puts e.next
StopIteration: iteration reached an end
 from (irb):105:in `next'
 from (irb):105
 from /usr/local/bin/irb:11:in `

'
>> e.rewind
=> #
>> puts e.next
dog
=> nil
枚举器是个对象,它可以维护状态。它能记住枚举的位置。迭代器是方法,调用完成以后就结束了。迭代器没有状态。枚举器是一个可枚举的对象。

用枚举器添加枚举的能力

你可以用枚举器给不具备枚举能力的对象添加枚举能力。把枚举器的 each 方法挂到任何迭代器上,你就可以使用枚举器在对象自己的迭代器上做枚举操作了,不管对象自己是否可以枚举。

你把枚举器挂到 String#bytes 方法上,你就给一个字符串对象添加了枚举能力。

下面这个类没有混合 Enumerable,不过它有一个迭代器方法:

module Music
  class Scale
    NOTES = %w{ c c# d d# e f f# g a a# b }

    def play
      NOTES.each {|note| yield note }
    end
  end
end
执行:

scale = Music::Scale.new
scale.play {|note| puts "Next note is #{note}" }
结果:

Next note is c
Next note is c#
Next note is d
Music::Scale 没有混合 Enumerable ,也没有定义 each 方法,所以:

scale.map {|note| note.upcase }
结果就会:

NoMethodError: unknown method `map' for #
想让 scale 拥有全部的枚举功能,你得混合 Enumerable ,还得把 play 的名字改成 each 。你也可以把一个枚举器挂到 scale 上。

为一个 scale 对象创建一个枚举器,绑到 play 方法上:

enum = scale.enum_for(:play)
enum 枚举器有 each 方法,这个方法做的迭代跟 scale 的 play 方法一样。不同的是,枚举器是一个枚举对象,它有 map,select,inject 这些来自 Enumerable 的方法。如果你使用枚举器,可以在完全不能枚举的对象上做枚举操作:

p enum.map {|note| note.upcase }
p enum.select {|note| note.include?('f') }
第一行输出:

["C", "C#", "D", "D#", "E", "F", "F#", "G", "A", "A#", "B"]
第二行输出:

["f", "f#"]
枚举器会用它附加到的那个对象上的 each 方法作为基础方法,枚举的工具包都会用到它。

枚举器是一个可以枚举的对象,它的 each 方法有点像是虹吸管,会从其它的对象上定义的迭代器那里抽取值。
枚举器的方法链

方法链在 Ruby 里很常见。

>> names
=> ["Matt", "Damon", "Scarlett", "Johansson"]

>> names.select {|n| n[0] < "M"}.map(&:upcase).join(", ")
=> "DAMON, JOHANSSON"
方法链一般在链的每个节点都会创建一个新的对象。

在上面的例子里,names 是一个字符串数组,Ruby 会额外再创建两个数组,一个是 select 创建的,一个是 map 创建的,join 又创建了一个字符串。枚举器在某些情况下可以缓和创建中间对象的这个问题。

节约中间对象

Enumerable 模块里面有很多方法在你不提供代码块调用它们的时候,这些方法都会返回一个枚举器。没理由直接用枚举器链接其它的方法。比如 names.each.inject  跟names.inject 一样,类似的还有names.map.select 跟你直接使用names.select 一样。map 枚举器不知道 map 到什么方法,也就是,它只能把原始的 array 的值下放到方法链。

不过有些情况可以链式调用方法:

>> names
=> ["Matt", "Damon", "Scarlett", "Johansson"]

>> names.select {|n| n[0] < "M"}.map(&:upcase).join(", ")
=> "DAMON, JOHANSSON"
with_index

>> ('a'..'z').map.with_index {|letter, i| [letter, i]}

=> [["a", 0], ["b", 1], ["c", 2], ["d", 3], ["e", 4], ["f", 5], ["g", 6], ["h", 7], ["i", 8], ["j", 9], ["k", 10], ["l", 11], ["m", 12], ["n", 13], ["o", 14], ["p", 15], ["q", 16], ["r", 17], ["s", 18], ["t", 19], ["u", 20], ["v", 21], ["w", 22], ["x", 23], ["y", 24], ["z", 25]]
Lazy 枚举器

lazy 枚举器可以有选择的枚举一个大集合。

试一下:

(1..Float::INFINITY).select {|n| n % 3 == 0}.first(10)
会一直运行 ...  select 不会结束。

这时候可以创建一个 lazy 枚举器:

>> (1..Float::INFINITY).lazy
=> #
然后再这样:

>> (1..Float::INFINITY).lazy.select {|n| n % 3 == 0 }
=> #:select>
>> (1..Float::INFINITY).lazy.select {|n| n % 3 == 0 }.first(10)
=> [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
另一种方法:

>> (1..Float::INFINITY).lazy.select {|n| n % 3 == 0 }
=> #:select>
>> (1..Float::INFINITY).lazy.select {|n| n % 3 == 0 }.first(10)
=> [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
FizzBuzz

如果数字可以被 15 整除,输出 “FizzBuzz”
不然如果数字可能被 3 整除,输出 “Fizz”
不然如果数字可能被 5 整除,输出 “Buzz”
不然就输出这个数字
def fb_calc(i)
  case 0
  when i % 15
    "FizzBuzz"
  when i % 3
    "Fizz"
  when i % 5
    "Buzz"
  else
    i.to_s
  end
end

def fb(n)
  (1..Float::INFINITY).lazy.map {|i| fb_calc(i) }.first(n)
end
试一下:

p fb(15)
会输出:

["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz"]

热门栏目