迭代器和可枚舉對象
迭代器的描述并不準確,像”期待一個關聯代碼塊的方法“這樣的描述更加準確一些。迭代器是 Ruby 的重要特性之一。當程序執行時,遇到迭代器總的 yield 語句時,程序控制流會從迭代器轉移到那個與迭代器想關聯的代碼塊中,程序執行完代碼塊之后,迭代器方法重新獲得控制權并從 yield 語句之后的第一條語句開始執行。
yield 語句像一個方法調用,后邊可以接零個或多個參數,這些值將會賦給對應的代碼塊的形參。
block_given?(同義詞 iterator? )方法可以判斷是否在調用該方法時帶有一個代碼塊,它們都是 Kernel 模塊定義的,所以表現的像全局函數一樣。
可枚舉對象
Array、Hash、Range 和許多其他的類都定義了 each 迭代器。大多數定義了 each 迭代器的類都包含了 Enumerable 模塊,它定義了許多更加特殊的迭代器,而它們都是基于 each 方法來實現的。其中包括 each_with_index、collect(也被稱為 map )、select、reject 和 inject 等等。
枚舉器
枚舉器是 Enumerable::Enumerator 的實例,其目的在于枚舉其他對象。雖然可以通過 new 操作符直接實例化這個類,但是通常情況下,我們并不會通過這種方式來創建枚舉器,而是使用 Object 類的 to_enum 或其同義詞 enum_for。
如果調用的時候沒有提供參數,那么這個枚舉器的 each 方法只是簡單的調用目標對象的 each 方法。例如,你有一個數組和一個方法,該方法期望一個可枚舉對象。因為數組可變,而且你不確定該方法是否會修改該數組,所以不想直接將數組傳遞給該方法。為了達到這個目的,與其創建一個該數組的深度防御拷貝,還不如直接調用它的 to_enum 方法。
process(data.to_enum)
你也可以給 to_enum 或 enum_for (顯得更自然一些)方法傳遞參數,第一個參數應該是一個符號,表示了一個迭代器方法(來自原先的對象)。這個返回的迭代器的 each 方法會調用那個迭代器方法。例如,在 Ruby1.9 中,String 類不是 Enumerable 的,但是它具有3個迭代器方法: each_char(同名 chars ),each_byte 和 each_line 。但如果我們想使用一個 Enumerable 方法,比如 map,而且基于 each_char 迭代器。我們可以這樣創建一個迭代器:
s = "hello"
s.enum_for(:each_char).map { |c| c.succ } # ["i", "f", "m", "m", "p"]
在 Ruby1.9 中,通常都不用顯式的使用 to_enmu 和 enum_for,因為以不帶代碼塊的方式調用內建的迭代器方法時(包括數值迭代器、each 和 Enumerable 相關方法時),它們都會自動的返回一個枚舉器。因此上邊的連個例子可以修改為:
process(data.each)
s="hello"
s.chars.map{ |c| c.succ }
當以不帶代碼塊的方式調用自己的迭代器時,可以通過返回 self.to_enum 的方法來實現上述行為。
def twice
if block_given?
yield
yield
else
self.to_enum(:twice)
end
end
Ruby1.9 中還定義了 with_index 方法,它只是返回一個新的枚舉器,為迭代添加索引形參。
s = "hello"
enumerator = s.each_char.with_index
enumerator.each do |char, index|
puts index.to_s + " " + char
end
外部迭代器
在 Ruby1.9 中迭代器還有一個重要作用就是外部迭代器: 外部迭代器。你可以通過反復調用一個枚舉器的 next 方法來遍歷一個集合的元素。
iterator = 9.downto(1)
begin
print iterator.next while true
rescue StopIteration
puts "...blastoff!"
end
外部迭代器的使用很簡單,每次需要另一個元素時調用 next 方法即可,遍歷完元素之后,next 拋出一個 StopIteration 異常。
Kernel.loop 方法包含了一個隱式的 rescue 從句,而且在 StopIteration 拋出時干凈利落的退出循環。前邊例子可以改寫如下:
iterator = 9.downto(1)
loop do
print iterator.next
end
puts "...blastoff!"
使用 rewind 方法可以是許多外部迭代器重新開始迭代,但是如果一個迭代器像 File 這樣從文件中順序讀入行的對象,那么調用 remind 方法并不能使其重新開始迭代??偟膩碚f,如果調用底層 Enumeralbe 對象的 each 方法并不能使其重新開始迭代,那么調用rewind 的方法也不會有效。
一個外部迭代器一旦啟動(第一次調用 next 方法之后),就不能在克隆和賦值該迭代器??梢钥寺∫粋€迭代器的典型時機是:next 被調用之前、StopIteration 被拋出之后,或者在 rewind 被調用之后。
外部迭代器比內部迭代器更加靈活,它們可以解決兩個迭代器的并行迭代的問題。
代碼塊
代碼塊的值
一個代碼塊的“返回值”就是它最后邊執行那個表達式的值。一般來說,你不應該將使用 return 關鍵字來從代碼塊中返回。一個位于代碼塊中的 return 將會導致包含該代碼塊的那個方法返回。如果你希望指定一個代碼塊的返回值應該使用 next。
變量作用域
代碼塊定義了一個新的變量作用域,但是在一個作用域中定義的局部變量,在該作用域中所有的代碼塊中都可見。
total = 0
data = [1, 2, 3]
data.each { |x| total += x }
puts total
從 Ruby1.9 開始,代碼塊的形參作用域范圍始終都在代碼塊內。如果使用 -w 選項,那么當一個代碼塊形參和一個已經存在的變量重名時,它就會發出警告。另外,你也可以聲明塊級局部變量,如下:
x = y = 0
1.upto(4) do |x;y|
y = x + 1
puts y*y
end
[x, y] # [0, 0]
傳遞實參
Ruby1.9 使代碼塊形參作用域范圍嚴格的局部于代碼塊本省,這就意味著,全局或實例變量不再是合理的代碼塊形參了。
與方法調用比起來,yield 關鍵字后邊的實參值傳遞給代碼塊形參的給類似于并行賦值規則,但是也部完全一樣。如果一個迭代器將兩個值傳遞給它的代碼塊,但是代碼塊只接受一個參數,Ruby 并不會像并行賦值一樣將兩個參數合并成為一個數組。
def two
yield 1, 2
end
two{ |x| p x } # 1
two{ |*x| p x } # [1, 2]
two{ |x,| p x } # 1
和并行賦值一樣,1.9中,無論代碼塊形參在參數列表的什么位置,都可以具有一個 * 前綴。
和方法調用一樣,yield 也允許不帶花括號的哈希作為其最后一個參數。
1.9中,最后一個代碼塊形參可以具有一個 & 前綴,表示它將接受與該代碼塊相關的任何代碼塊。
代碼塊形參和方法形參有一個重要的區別就是,代碼塊形參不允許有默認值。一種創建 proc 對象的字面量語法才允許有默認值。
[1, 2, 3].each &->(x, y=10) { print x*y }
改變控制流
return
當 return 語句位于一個代碼塊的時候(無論嵌套多深),它總會使得外圍的方法返回,即它不僅會使得代碼塊返回,還會使得調用代碼塊的那個迭代器返回,而且它還會使得外圍方法返回。
def find(array, target)
array.each_with_index do |element, index|
puts "haha"
return index if (element == target)
end
nil
end
值得注意的是,普通代碼塊和 lambda 表達式中的 return 行為并不一致。
break
當被用在一個代碼塊中時,break 不僅將控制權傳遞出代碼塊,而且傳遞到調用代碼塊的迭代器之外。和 return 不一樣,break 并不會使外圍方法返回。
arr = [1, 2, 3, 4, 5]
arr.each do |i|
break if i == 4
puts i
end
break 只能出現在一個詞法上外圍循環或代碼塊里,其他任何上下問使用 break 都會導致一個 LocalJumpError。
break 可以為他所跳出的循環或迭代器指定一個值。如果 break 表達式后邊沒有表達式,那么循環表達式和迭代器的返回值就是 nil。
next
next 語句使一個循環或迭代器結束當前的迭代,開始下一輪迭代。當用在一個代碼塊里的時候,next 使代碼塊立即結束,將控制權返回給迭代器的方法。
next 后邊也可以接一個表達式,當用在一個循環當中,next 之后的任何值都會被忽略。當用在代碼快中時,next 之后的值會被當作 yield 語句的返回值。
redo
redo 將控制權傳遞到循環或代碼塊的開頭,重新開始當前迭代。它不會重新測試循環條件,也不會獲取迭代器的下一個元素。redo 語句并不是一個常用語句,它一種用法是從用戶輸入錯誤中恢復過來。
puts "Please enter the first word you think of"
words = %w(apple banana cherry)
response = words.collect do |word|
print word + "> "
response = gets.chop
if response.size == 0
word.upcase!
redo
end
response
end
throw 和 catch
throw 和 catch 是 Kernel 模塊的方法。throw 不僅可以跳出當前循環或代碼塊,而且可以向外跳出任意數量級,使與 catch 一同定義的代碼塊退出。
下面展示了如何“跳出”嵌套循環:
for matrix in data do
catch :missing_data do
for row in matrix do
for value in row do
throw :missing_data unless value
puts "#{value}"
end
end
end
end