迭代器和可枚舉對象
迭代器的描述并不準(zhǔn)確酗宋,像”期待一個(gè)關(guān)聯(lián)代碼塊的方法“這樣的描述更加準(zhǔn)確一些。迭代器是 Ruby 的重要特性之一疆拘。當(dāng)程序執(zhí)行時(shí)蜕猫,遇到迭代器總的 yield 語句時(shí),程序控制流會(huì)從迭代器轉(zhuǎn)移到那個(gè)與迭代器想關(guān)聯(lián)的代碼塊中哎迄,程序執(zhí)行完代碼塊之后回右,迭代器方法重新獲得控制權(quán)并從 yield 語句之后的第一條語句開始執(zhí)行。
yield 語句像一個(gè)方法調(diào)用漱挚,后邊可以接零個(gè)或多個(gè)參數(shù)翔烁,這些值將會(huì)賦給對應(yīng)的代碼塊的形參。
block_given?(同義詞 iterator? )方法可以判斷是否在調(diào)用該方法時(shí)帶有一個(gè)代碼塊旨涝,它們都是 Kernel 模塊定義的蹬屹,所以表現(xiàn)的像全局函數(shù)一樣。
可枚舉對象
Array白华、Hash慨默、Range 和許多其他的類都定義了 each 迭代器。大多數(shù)定義了 each 迭代器的類都包含了 Enumerable 模塊弧腥,它定義了許多更加特殊的迭代器厦取,而它們都是基于 each 方法來實(shí)現(xiàn)的。其中包括 each_with_index管搪、collect(也被稱為 map )蒜胖、select、reject 和 inject 等等抛蚤。
枚舉器
枚舉器是 Enumerable::Enumerator 的實(shí)例,其目的在于枚舉其他對象寻狂。雖然可以通過 new 操作符直接實(shí)例化這個(gè)類岁经,但是通常情況下,我們并不會(huì)通過這種方式來創(chuàng)建枚舉器蛇券,而是使用 Object 類的 to_enum 或其同義詞 enum_for缀壤。
如果調(diào)用的時(shí)候沒有提供參數(shù),那么這個(gè)枚舉器的 each 方法只是簡單的調(diào)用目標(biāo)對象的 each 方法纠亚。例如塘慕,你有一個(gè)數(shù)組和一個(gè)方法,該方法期望一個(gè)可枚舉對象蒂胞。因?yàn)閿?shù)組可變图呢,而且你不確定該方法是否會(huì)修改該數(shù)組,所以不想直接將數(shù)組傳遞給該方法。為了達(dá)到這個(gè)目的蛤织,與其創(chuàng)建一個(gè)該數(shù)組的深度防御拷貝赴叹,還不如直接調(diào)用它的 to_enum 方法。
process(data.to_enum)
你也可以給 to_enum 或 enum_for (顯得更自然一些)方法傳遞參數(shù)指蚜,第一個(gè)參數(shù)應(yīng)該是一個(gè)符號乞巧,表示了一個(gè)迭代器方法(來自原先的對象)。這個(gè)返回的迭代器的 each 方法會(huì)調(diào)用那個(gè)迭代器方法摊鸡。例如绽媒,在 Ruby1.9 中,String 類不是 Enumerable 的免猾,但是它具有3個(gè)迭代器方法: each_char(同名 chars )是辕,each_byte 和 each_line 。但如果我們想使用一個(gè) Enumerable 方法掸刊,比如 map免糕,而且基于 each_char 迭代器。我們可以這樣創(chuàng)建一個(gè)迭代器:
s = "hello"
s.enum_for(:each_char).map { |c| c.succ } # ["i", "f", "m", "m", "p"]
在 Ruby1.9 中忧侧,通常都不用顯式的使用 to_enmu 和 enum_for石窑,因?yàn)橐圆粠Тa塊的方式調(diào)用內(nèi)建的迭代器方法時(shí)(包括數(shù)值迭代器、each 和 Enumerable 相關(guān)方法時(shí))蚓炬,它們都會(huì)自動(dòng)的返回一個(gè)枚舉器松逊。因此上邊的連個(gè)例子可以修改為:
process(data.each)
s="hello"
s.chars.map{ |c| c.succ }
當(dāng)以不帶代碼塊的方式調(diào)用自己的迭代器時(shí),可以通過返回 self.to_enum 的方法來實(shí)現(xiàn)上述行為肯夏。
def twice
if block_given?
yield
yield
else
self.to_enum(:twice)
end
end
Ruby1.9 中還定義了 with_index 方法经宏,它只是返回一個(gè)新的枚舉器,為迭代添加索引形參驯击。
s = "hello"
enumerator = s.each_char.with_index
enumerator.each do |char, index|
puts index.to_s + " " + char
end
外部迭代器
在 Ruby1.9 中迭代器還有一個(gè)重要作用就是外部迭代器: 外部迭代器烁兰。你可以通過反復(fù)調(diào)用一個(gè)枚舉器的 next 方法來遍歷一個(gè)集合的元素。
iterator = 9.downto(1)
begin
print iterator.next while true
rescue StopIteration
puts "...blastoff!"
end
外部迭代器的使用很簡單徊都,每次需要另一個(gè)元素時(shí)調(diào)用 next 方法即可沪斟,遍歷完元素之后,next 拋出一個(gè) StopIteration 異常暇矫。
Kernel.loop 方法包含了一個(gè)隱式的 rescue 從句主之,而且在 StopIteration 拋出時(shí)干凈利落的退出循環(huán)。前邊例子可以改寫如下:
iterator = 9.downto(1)
loop do
print iterator.next
end
puts "...blastoff!"
使用 rewind 方法可以是許多外部迭代器重新開始迭代李根,但是如果一個(gè)迭代器像 File 這樣從文件中順序讀入行的對象槽奕,那么調(diào)用 remind 方法并不能使其重新開始迭代》拷危總的來說粤攒,如果調(diào)用底層 Enumeralbe 對象的 each 方法并不能使其重新開始迭代所森,那么調(diào)用rewind 的方法也不會(huì)有效。
一個(gè)外部迭代器一旦啟動(dòng)(第一次調(diào)用 next 方法之后)琼讽,就不能在克隆和賦值該迭代器必峰。可以克隆一個(gè)迭代器的典型時(shí)機(jī)是:next 被調(diào)用之前钻蹬、StopIteration 被拋出之后吼蚁,或者在 rewind 被調(diào)用之后。
外部迭代器比內(nèi)部迭代器更加靈活问欠,它們可以解決兩個(gè)迭代器的并行迭代的問題肝匆。
代碼塊
代碼塊的值
一個(gè)代碼塊的“返回值”就是它最后邊執(zhí)行那個(gè)表達(dá)式的值。一般來說顺献,你不應(yīng)該將使用 return 關(guān)鍵字來從代碼塊中返回旗国。一個(gè)位于代碼塊中的 return 將會(huì)導(dǎo)致包含該代碼塊的那個(gè)方法返回。如果你希望指定一個(gè)代碼塊的返回值應(yīng)該使用 next注整。
變量作用域
代碼塊定義了一個(gè)新的變量作用域能曾,但是在一個(gè)作用域中定義的局部變量,在該作用域中所有的代碼塊中都可見肿轨。
total = 0
data = [1, 2, 3]
data.each { |x| total += x }
puts total
從 Ruby1.9 開始寿冕,代碼塊的形參作用域范圍始終都在代碼塊內(nèi)。如果使用 -w 選項(xiàng)椒袍,那么當(dāng)一個(gè)代碼塊形參和一個(gè)已經(jīng)存在的變量重名時(shí)驼唱,它就會(huì)發(fā)出警告。另外驹暑,你也可以聲明塊級局部變量玫恳,如下:
x = y = 0
1.upto(4) do |x;y|
y = x + 1
puts y*y
end
[x, y] # [0, 0]
傳遞實(shí)參
Ruby1.9 使代碼塊形參作用域范圍嚴(yán)格的局部于代碼塊本省,這就意味著优俘,全局或?qū)嵗兞坎辉偈呛侠淼拇a塊形參了京办。
與方法調(diào)用比起來,yield 關(guān)鍵字后邊的實(shí)參值傳遞給代碼塊形參的給類似于并行賦值規(guī)則帆焕,但是也部完全一樣惭婿。如果一個(gè)迭代器將兩個(gè)值傳遞給它的代碼塊,但是代碼塊只接受一個(gè)參數(shù)视搏,Ruby 并不會(huì)像并行賦值一樣將兩個(gè)參數(shù)合并成為一個(gè)數(shù)組。
def two
yield 1, 2
end
two{ |x| p x } # 1
two{ |*x| p x } # [1, 2]
two{ |x,| p x } # 1
和并行賦值一樣县袱,1.9中浑娜,無論代碼塊形參在參數(shù)列表的什么位置,都可以具有一個(gè) * 前綴式散。
和方法調(diào)用一樣筋遭,yield 也允許不帶花括號的哈希作為其最后一個(gè)參數(shù)。
1.9中,最后一個(gè)代碼塊形參可以具有一個(gè) & 前綴漓滔,表示它將接受與該代碼塊相關(guān)的任何代碼塊编饺。
代碼塊形參和方法形參有一個(gè)重要的區(qū)別就是,代碼塊形參不允許有默認(rèn)值响驴。一種創(chuàng)建 proc 對象的字面量語法才允許有默認(rèn)值透且。
[1, 2, 3].each &->(x, y=10) { print x*y }
改變控制流
return
當(dāng) return 語句位于一個(gè)代碼塊的時(shí)候(無論嵌套多深),它總會(huì)使得外圍的方法返回豁鲤,即它不僅會(huì)使得代碼塊返回秽誊,還會(huì)使得調(diào)用代碼塊的那個(gè)迭代器返回,而且它還會(huì)使得外圍方法返回琳骡。
def find(array, target)
array.each_with_index do |element, index|
puts "haha"
return index if (element == target)
end
nil
end
值得注意的是锅论,普通代碼塊和 lambda 表達(dá)式中的 return 行為并不一致。
break
當(dāng)被用在一個(gè)代碼塊中時(shí)楣号,break 不僅將控制權(quán)傳遞出代碼塊最易,而且傳遞到調(diào)用代碼塊的迭代器之外。和 return 不一樣炫狱,break 并不會(huì)使外圍方法返回藻懒。
arr = [1, 2, 3, 4, 5]
arr.each do |i|
break if i == 4
puts i
end
break 只能出現(xiàn)在一個(gè)詞法上外圍循環(huán)或代碼塊里,其他任何上下問使用 break 都會(huì)導(dǎo)致一個(gè) LocalJumpError毕荐。
break 可以為他所跳出的循環(huán)或迭代器指定一個(gè)值束析。如果 break 表達(dá)式后邊沒有表達(dá)式,那么循環(huán)表達(dá)式和迭代器的返回值就是 nil憎亚。
next
next 語句使一個(gè)循環(huán)或迭代器結(jié)束當(dāng)前的迭代员寇,開始下一輪迭代。當(dāng)用在一個(gè)代碼塊里的時(shí)候第美,next 使代碼塊立即結(jié)束蝶锋,將控制權(quán)返回給迭代器的方法。
next 后邊也可以接一個(gè)表達(dá)式什往,當(dāng)用在一個(gè)循環(huán)當(dāng)中扳缕,next 之后的任何值都會(huì)被忽略。當(dāng)用在代碼快中時(shí)别威,next 之后的值會(huì)被當(dāng)作 yield 語句的返回值躯舔。
redo
redo 將控制權(quán)傳遞到循環(huán)或代碼塊的開頭,重新開始當(dāng)前迭代省古。它不會(huì)重新測試循環(huán)條件粥庄,也不會(huì)獲取迭代器的下一個(gè)元素。redo 語句并不是一個(gè)常用語句豺妓,它一種用法是從用戶輸入錯(cuò)誤中恢復(fù)過來惜互。
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 不僅可以跳出當(dāng)前循環(huán)或代碼塊,而且可以向外跳出任意數(shù)量級训堆,使與 catch 一同定義的代碼塊退出描验。
下面展示了如何“跳出”嵌套循環(huán):
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