迭代器與程式區塊


每次呼叫方法時,其實會涉及四個部份:
  • 訊息接收者
  • . 運算
  • 訊息名稱
  • 程式區塊

至今看過幾個具有程式區塊的例子。例如loop方法:
loop do
    puts "Orz"
end

如果你要自行定義相同功能的方法,可以如下:
def forever
while true
yield
end
end

forever do
puts "Orz"
end

以上定義了forever迭代器,如果呼叫forever時緊接著包括程式區塊,forever方法中執行yield一次,就會執行指定的程式區塊一次,do..end也可以使用{}取代。例如:
forever { puts "Orz" }

{}的區塊寫法與do..end是幾乎相同的意義,慣例中,在一行中可寫完的區塊可使用{},有傳回值的時候也可使用 {},do..end則通常用於具有邊際效應的程式區塊。

如果區塊中需要提供額外資訊,則可以定義
區塊參數(Block parameter)。例如:
def for_with_index(from, to, step)
for i in from..to
yield(i)
end
end

for_with_index(0, 10, 2) do |index|
puts index
end

區塊參數以||包括,在yield時若傳入引數,執行區塊時就會設定給區塊參數,若有多個區塊參數,則以逗號區隔,yield指定引數時也是以逗號區隔。區塊參數也可以設定預設引數或者是接受不定長度引數的區塊參數,規則與定義方法參數相同,可參考
def 定義方法 中的說明,只不過無論yield時有無指定引數,區塊參數都是非必要的。例如:
class Integer
def my_times
for i in 0...self
yield(i)
end
end
end

5.my_times do
puts "XD"
end

5.my_times do |i|
puts "#{i} - XD"
end

程式區塊若有傳回值,則會成為yield的傳回值。例如,想實作陣列的reduce方法,可以如下:
class Array
def my_reduce(value = 0)
for i in 0...self.length
# 不同環境中 self 代表不同對象,在這邊 self 代表陣列實例。
value = yield(value, self[i])
end
value
end
end

puts [1, 2, 3, 4, 5].my_reduce { |sum, value|
sum + value
}

如果程式區塊中撰寫了break、next或redo,相當於在yield處直接break、next或redo,例如:
class Integer
def my_times
# 不同環境中 self 代表不同對象,在這邊 self 代表 Integer 實例。
for i in 0...self
yield(i)
end
end
end

5.my_times do |i|
puts "#{i} - XD"
if i == 3
break
end


就某種程度來說,這相當於這麼寫:
class Integer
    def my_times
        for i in (0..self - 1)
            puts "#{i} - XD"
            if i == 3
                break
            end
        end
    end
end

就底下這個例子而言,使用{}與do..end看不出什麼差別:

>> [1, 2, 3].each { |element|
?>     print element
>> }
123=> [1, 2, 3]
>> [1, 2, 3].each do |element|
?>     print element
>> end
123=> [1, 2, 3]
>>


但{}與do..end的寫法,在結合其它方法時,要注意執行順序。例如:
>> puts [1, 2, 3].each { |element|
?>     print element
>> }
1231
2
3
=> nil
>> puts [1, 2, 3].each do |element|
?>     print element
>> end
#<Enumerator:0x267d1e8>
=> nil
>>


each執行完會傳回原陣列,因此第一個例子在顯示完123後,傳回的陣列被puts再分別換行顯示出來。第二個例子則是被照以下順序解釋:
>> puts([1, 2, 3].each) do |element|
?>     print element
>> end
#<Enumerator:0x266d720>
=> nil
>> puts([1, 2, 3].each) { |element|
?>     print element
>> }

#<Enumerator:0x25a2008>
=> nil
>>


[1, 2, 3].each會先被執行,然後進行puts,其實你的區塊是傳給了puts,而不是傳給each,當你傳遞區塊給一個方法時,而該方法沒有yield,則傳遞的區塊只是被忽略。

如果你在方法最後一個參數設定&開頭的參數,則會使用區塊建立Proc物件傳入,你可以用以判斷是否有傳入區塊,或者將代表區塊的物件,再傳給另一方法。例如:
# encoding: Big5
def for_with_index(from, to, step, &block)
if !block
return
end
for i in from..to
yield(i)
end
end

for_with_index(0, 10, 2) do |index|
puts index
end

puts "沒有區塊的for_with_index 開始"
for_with_index(0, 10, 2)
puts "沒有區塊的for_with_index 結束"

Proc有個call方法,用以執行Proc物件內含的程序,上例中,實際上yield(i)該行,也可以改為block.call(i)。