跳至內容

Ruby 程式設計/參考/物件/Enumerable

來自華夏公益教科書,開放的書籍,開放的世界

Enumerable

[編輯 | 編輯原始碼]

Enumerator 在 Ruby 中以 Enumerable::Enumerator 的形式出現在 1.8.x 中,而在 1.9.x 中則以 (僅) Enumerator 的形式出現。

Enumerator 的形式

[編輯 | 編輯原始碼]

Enumerator 有多種不同的使用方法

  • 作為“each”的代理
  • 作為塊中值的來源
  • 作為外部迭代器

1. 作為“each”的代理

[編輯 | 編輯原始碼]

這是在 ruby 1.8 中引入的 Enumerator 的第一種使用方法。它解決了以下問題:Enumerable 方法如 #map 和 #select 會在你的物件上呼叫 #each,但如果你想使用其他方法(如 #each_byte 或 #each_with_index)進行迭代怎麼辦?

Enumerator 是一個簡單的代理物件,它接收對 #each 的呼叫並將它重定向到基礎物件上的不同方法。

require 'enumerator'   # needed in ruby <= 1.8.6 only

src = "hello"
puts src.enum_for(:each_byte).map { |b| "%02x" % b }.join(" ")

對 'enum_for'(或等效地 'to_enum')的呼叫建立了 Enumerator 代理。它是以下內容的簡寫

newsrc = Enumerable::Enumerator.new(src, :each_byte)
puts newsrc.map { |b| "%02x" % b }.join(" ")

在 ruby 1.9 中,Enumerable::Enumerator已更改為Enumerator

2. 作為塊中值的來源

[編輯 | 編輯原始碼]

在 ruby 1.9 中,Enumerator.new 可以接收一個塊,該塊在呼叫 #each 時執行,並直接產生值。

block =  Enumerator.new {|g| g.yield 1; g.yield 2; g.yield 3}

block.each do |item|
  puts item
end

“g << 1”是“g.yield 1”的替代語法

沒有使用 Fiber 或 Continuation 等高階語言功能,這種形式的 Enumerator 很容易移植到 ruby 1.8

它與建立產生值的自定義物件非常相似

block = Object.new
def block.each
  yield 1; yield 2; yield 3
end

block.each do |item|
  puts item
end

但是,它也為後面將要描述的可列舉物件的“延遲”評估奠定了基礎。

3. 作為外部迭代器

[編輯 | 編輯原始碼]

ruby 1.9 還允許你反轉 Enumerator,使其成為值的“拉取”源,有時也稱為“外部迭代”。仔細觀察它與上一個例子的區別

block =  Enumerator.new {|g| g.yield 1; g.yield 2; g.yield 3}

while item = block.next
  puts item
end

控制流來回切換,第一次呼叫 #next 時會建立一個 Fiber,它儲存呼叫之間的狀態。因此,它的效率低於直接使用 #each 進行迭代。

當你呼叫 #next 且沒有更多值時,會丟擲一個StopIteration異常。這會被 while 迴圈靜默捕獲。StopIterationIndexError的子類,而IndexError.

又是

require 'generator'
block = Generator.new {|g| g.yield 1; g.yield 2; g.yield 3}

while block.next?
  puts block.next
end

StandardError

的子類

ruby 1.8 中最接近的等效功能是 Generator,它是使用 Continuations 實現的。

延遲評估

  Enumerator.new do |y|
    source.each do |input|     # filter INPUT
      ...
      y.yield output           # filter OUTPUT
    end
  end

[編輯 | 編輯原始碼]

class Enumerator
  def defer(&blk)
    self.class.new do |y|
      each do |*input|
        blk.call(y, *input)
      end
    end
  end
end

在帶有塊的 Enumerator 中,要產生的目標會作為顯式引數傳遞。這使得有可能設定方法呼叫的鏈,以便每個值從左到右傳遞到整個鏈,而不是在每一步都構建中間值陣列。

res = (1..1_000_000_000).to_enum.
  defer { |out,inp| out.yield inp if inp % 2 == 0 }.   # like select
  defer { |out,inp| out.yield inp+100 }.               # like map
  take(10)
p res

基本模式是帶有塊的 Enumerator,它處理輸入值併為每個輸入值產生(零個或多個)輸出值。

所以,讓我們把它包裝在一個便利方法中

這個新的方法 'defer' 可以用作 select 和 map 的“延遲”形式。它不會構建值陣列並在最後返回該陣列,而是立即產生每個值。這意味著你可以更快地獲得答案,並且它可以處理巨大甚至無限長的列表。示例

雖然我們從一個包含十億個專案的列表開始,但在最後我們只使用了生成的第一個 10 個值,因此我們在此完成後停止迭代。

你可以在 ruby 1.8 中使用相同的功能,使用facets 庫。為了方便起見,它還提供了一個Denumberable 模組,其中包含常見 Enumerable 方法(如 map、select 和 reject)的延遲版本。

>> a = ["foo","bar","baz"]
=> ["foo", "bar", "baz"]
>> b = a.each_with_index
=> #<Enumerable::Enumerator:0xb7d7cadc>
>> b.each { |args| p args }
["foo", 0]
["bar", 1]
["baz", 2]
=> ["foo", "bar", "baz"]
>> 

返回 Enumerators 的方法[編輯 | 編輯原始碼]從 1.8.7 開始,許多 Enumerable 方法如果未提供塊,將返回 Enumerator。

src = "hello"
puts src.each_byte.map { |b| "%02x" % b }.join(" ")

這意味著通常你不需要顯式呼叫

=> ["foo", "bar", "baz"]
>> b = a.select
=> #<Enumerable::Enumerator:0xb7d6cfb0>
>> b.each { |arg| arg < "c" }
=> ["bar", "baz"]
>> 

enum_for

本頁面上的第一個示例簡化為

工單 707

enum_for 在 Strictly Untyped 部落格中

Generator 在 Anthony Lewis 的部落格中

array = ['Superman','Batman','The Hulk']

array.each_with_index do |item,index|
  puts "#{index} -> #{item}"
 end

# will print
# 0 -> Superman
# 1 -> Batman
# 2 -> The Hulk

each_with_index

[編輯 | 編輯原始碼]

each_with_index 會使用項及其索引呼叫其塊。

range = 1 .. 10

# find the even numbers

array = range.find_all { |item| item % 2 == 0 }

# returns [2,4,6,8,10]
array = ['Superman','Batman','Catwoman','Wonder Woman']

array = array.find_all { |item| item =~ /woman/ }

# returns ['Catwoman','Wonder Woman']
find_all
華夏公益教科書