# **第 11 章 塊**
Ruby 中大量使用了塊(block)。塊原本只是為了循環而產生的語法結構,但現在程序中許多地方也都使用了塊。因此,如何靈活地使用塊,也是 Ruby 的重點之一。
下面就讓我們來討論一下塊的作用及其用途。
### **11.1 塊是什么**
塊 1 就是在調用方法時,能與參數一起傳遞的多個處理的集合。之前在介紹 `each` 方法、`time` 方法等與循環有關的部分時,我們就已經接觸過塊。接收塊的方法會執行必要次數的塊。塊的執行次數由方法本身決定,因此不需事前指定,甚至有可能一次都不執行。
1有時也稱代碼塊。——譯者注
在下面的例子中,我們使用 `each` 方法,把保存在 `Array` 對象中的各個整數依次取 2 次冪后輸出。`do` 和 `end` 之間的部分就是所謂的塊。在本例中,塊總共被執行了 5 次。
~~~
[1, 2, 3, 4, 5].each do |i|
puts i ** 2
end
~~~
正如在第 7 章中所介紹的那樣,我們把這樣的方法調用稱為“調用帶塊的方法”或者“調用塊”。塊的調用方法一般采用以下形式。
**對象. 方法名( 參數列表) `do` | 塊變量 |
希望循環的處理
`end`**
或者
**對象. 方法名( 參數列表) { | 塊變量 |
希望循環的處理
}**
塊的開頭是塊變量。塊變量就是在執行塊的時候,從方法傳進來的參數。不同方法的塊變量個數也不相同。例如,在 `Array#each` 方法中,數組的元素會作為塊變量被逐個傳遞到塊中。而在 `Array#each_with_index` 方法中,則是 [ 元素 , 索引 ] 兩個值被傳遞到塊中。
> **執行示例**

而在第 6 章中介紹的 `loop` 方法則不需要傳遞塊變量。
### **11.2 塊的使用方法**
### **11.2.1 循環**
在 Ruby 中,我們常常使用塊來實現循環。在接收塊的方法中,實現了循環處理的方法稱為迭代器(iterator)。`each` 方法就是一個典型的迭代器。
在下面的例子中,我們把數組的各個元素轉換為大寫后輸出。
~~~
alphabet = ["a", "b", "c", "d", "e"]
alphabet.each do |i|
puts i.upcase
end
~~~
和數組一樣,散列也能將元素一個個拿出來,但與數組不同的是,散列會將 `[key, value]` 的組合作為數組來提取元素。如代碼清單 11.1 所示,可以成對地提取散列的全部鍵、值。本例中使用 `pair[1]` 提取并合計了散列的值,提取散列的鍵時則可以使用 `pair[0]`。
**代碼清單 11.1 hash_each.rb**
~~~
sum = 0
outcome = {"參加費"=>1000, "掛件費用"=>1000, "聯歡會費用"=>4000}
outcome.each do |pair|
sum += pair[1] # 指定值
end
puts "合計:#{sum}"
~~~
在接收塊變量時,多重賦值規則也是同樣適用的。我們稍微把代碼清單 11.1 的程序修改一下,使之變成代碼清單 11.2 那樣,這樣一來,鍵、值就可以被分別賦值給不同的變量了。
**代碼清單 11.2 hash_each2.rb**
~~~
sum = 0
outcome = {"參加費"=>1000, "掛件費用"=>1000, "聯歡會費用"=>4000}
outcome.each do |item, price|
sum += price
end
puts "合計:#{sum}"
~~~
`File` 對象被用于讀寫文件的內容。使用 `File` 對象可將文件數據從頭到尾讀取出來。
根據文件內容的不同,我們需要考慮是以字符為單位,還是以行為單位來做讀取處理。代碼清單 11.3 是使用了 `File` 類的 `each` 方法的一個程序示例,它會把 sample.txt 文件的內容按順序逐行讀取出來并輸出。
**代碼清單 11.3 file_each.rb**
~~~
file = File.open("sample.txt")
file.each_line do |line|
print line
end
file.close
~~~
除了 `each_line` 方法外,`File` 對象中還有以字符為單位來循環讀取數據的 `each_char` 方法、以及以字節為單位進行循環讀取的 `each_byte` 方法等等。而其他對象也有很多以 `each_XX` 命名的循環讀取數據的方法。
### **11.2.2 隱藏常規處理**
上文中我們介紹了將塊用于循環的迭代器的例子。但正如本章開頭所介紹的那樣,除了迭代器以外,塊還被廣泛使用在其他地方。其中一個用法就是確保后處理被執行。下面我們來看一個典型的例子——`File.open` 方法。`File.open` 方法在接收塊后,會將 `File` 對象作為塊變量,并執行一次塊。這里,我們可以使用塊把代碼清單 11.3 改寫為代碼清單 11.4 那樣。
**代碼清單 11.4 file_open.rb**
~~~
File.open("sample.txt") do |file|
file.each_line do |line|
print line
end
end
~~~
與改寫之前的程序相比,`File` 對象讀取數據的部分一樣,不同點在于沒有了最后的 `close` 方法的調用。如果使用完打開的文件后沒有將文件關閉的話,有可能會產生其他程序無法打開該文件,或者到達一次性可打開的文件數的上限時無法再打開新文件等問題。而在代碼清單 11.4 的程序中,即使遇到無法打開文件等錯誤也可以正常關閉文件,因為塊內部進行了程序清單 11.5 那樣的處理。
**代碼清單 11.5 file_open_no_block.rb**
~~~
file = File.open("sample.txt")
begin
file.each_line do |line|
print line
end
ensure
file.close
end
~~~
`File.open` 方法使用塊時,塊內部的處理完畢并跳出方法前,文件會被自動關閉,因此就不需要像代碼清單 11.3 那樣使用 `File#close` 方法。
文件使用完畢后,由方法執行關閉操作,而我們只需將必要的處理記述在塊中即可。這樣一來可以減少程序的代碼量,二來可以防止忘記關閉文件等錯誤的發生。
### **11.2.3 替換部分算法**
下面我們再來介紹一個塊的常見用法。這一次我們以數組排序為例,來了解一下指定處理順序時塊的使用方法。
-
**自定義排列順序**
`Array` 類的 `sort` 方法是對數組內元素進行排序的方法。對數組元素進行排序,可以采取多種方法。
-
**按數字的大小順序**
-
**按字母順序**
-
**按字符串的長度順序**
-
**按數組元素的合計值的大小順序**
如果按照這樣的條件分別定義相應的排序方法,就會使方法的數量過多,不便于記憶。因此,在 `Array#sort` 方法中,元素的排序步驟由方法決定,用戶只能指定元素間關系的比較邏輯。
`Array#sort` 方法沒有指定塊時,會使用 `<=>` 運算符對各個元素進行比較,并根據比較后的結果進行排序。`<=>` 運算符的返回值為`-1`、`0`、`1` 中的一個。
**表 11.1 a <=> b 的結果**
| `a <>` 時 | -1(比 0 小) |
|-----|-----|
| `a == b` 時 | 0 |
| `a > b` 時 | 1(比 0 大) |
使用 `<=>` 運算符比較字符串時,會按照字符編碼的順序進行比較。比較字母時,會按先大寫字母后小寫字母的順序排列。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort
p sorted #=> ["PHP", "Perl", "Python", "ruby"]
~~~
我們可以通過調用塊來指定排列順序。下面的例子與不使用塊時的執行結果是一樣的。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort{ |a, b| a <=> b }
p sorted #=> ["PHP", "Perl", "Python", "ruby"]
~~~
在 `sort` 方法的末尾添加了塊 `{ |a, b| a <=> b }`,sort 方法會根據塊的執行結果判斷元素的大小關系。當需要比較元素的大小關系時,塊中需要比較的兩個對象就會被作為塊變量調用。對塊變量 `a` 和 `b` 進行比較后,數組整體就會按該順序排列。
在這里,我們需要注意塊中最后一個表達式的值就是塊的執行結果,因此 `<=>` 運算符必須在最后一行使用。
> **備注** 塊的最后一個表達式不是指塊的最后一行表達式,而是指在塊中最后執行的表達式。
按字符串的長度排序時,可以采用如下方法。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort{ |a, b| a.length <=> b.length }
p sorted #=> ["PHP", "ruby", "Perl", "Python"]
~~~
在之前的例子中,我們只是單純地比較了字符串 `a`、`b`,這里我們使用 `String#length` 方法,來比較字符串的長度。用 `<=>` 運算符比較數值時,得到的是由小到大的排列順序,因此,比較字符串長度時,結果就是按照由短到長的順序進行排列。
像這樣,塊經常被用來在 `sort` 方法中實現自定義排列順序。
-
**預先取出排序所需的信息**
我們再來詳細看看 `sort` 方法的塊。每次比較元素時,`sort` 方法都會調用一次將兩個元素作為塊變量的塊。這里,我們仍以剛才介紹的按字符串長度排序的程序為例,來看看程序調用了 `length` 方法多少次。
**代碼清單 11.6 sort_comp_count.rb**
~~~
ary = %w(
Ruby is a open source programming language with a focus
on simplicity and productivity. It has an elegant syntax
that is natural to read and easy to write
)
call_num = 0 # 塊的調用次數
sorted = ary.sort do |a, b|
call_num += 1 # 累加塊的調用次數
a.length <=> b.length
end
puts "排序結果 #{sorted}"
puts "數組的元素數量 #{ary.length}"
puts "調用塊的次數 #{call_num}"
~~~
> **執行示例**
~~~
> ruby sort_comp_count.rb
排序結果 ["a", "a", "on", "to", "It", "to", "is", "an", ......]
數組的元素數量 28
調用塊的次數 91
~~~
可以看出,在這個例子中,我們對 28 個元素進行了排序,塊總共被調用了 91 次。由于每調用 1 次塊,`length` 方法就會被調用 2 次,因此最終就會被調用 182 次。而實際上,我們只需對所有的字符串都調用 1 次 `length` 方法,然后再用得出的結果進行排序就可以了。像這樣,在能夠通過 `< = >` 運算符對轉換后的結果進行比較的情況下,使用 `sort_by` 方法會使排序更加有效率。
~~~
ary = %w(
Ruby is a open source programming language with a focus
on simplicity and productivity. It has an elegant syntax
that is natural to read and easy to write
)
sorted = ary.sort_by{ |item| item.length }
p sorted
~~~
`sort_by` 方法會將每個元素在塊中各調用一次,然后再根據這些結果做排序處理。這種情況下,雖然比較的次數不變,但獲取排序所需要的信息的次數(本例中為 28 次)只需與元素個數一樣就可以了。
總結一下,元素排序算法中公共的部分由方法本身提供,我們則可以用塊來替換方法中元素排列的順序(或者取得用于比較的信息),或者根據不同的目的來替換需要更改的部分。
### **11.3 定義帶塊的方法**
在第 7 章中我們簡單地介紹了如何定義帶塊的方法,接下來我們就來詳細地討論一下。
### **11.3.1 執行塊**
首先讓我們重溫一下第 7 章中的 `myloop` 方法(代碼清單 11.7)。
**代碼清單 11.7 myloop.rb**
~~~
def myloop
while true
yield # 執行塊
end
end
num = 1 # 初始化num
myloop do
puts "num is #{num}" # 輸出num
break if num > 100 # num 超過100 后跳出循環
num *= 2 # num 乘2
end
~~~
`myloop` 方法在執行 `while` 循環的同時執行了 `yield` 關鍵字,`yield` 關鍵字的作用就是執行方法的塊。因為這個 `while` 循環的條件固定為 `true`,所以會無限循環地執行下去,但只要在塊里調用 `break`,就可以隨時中斷 `myloop` 方法,來執行后面的處理。
### **11.3.2 傳遞塊參數,獲取塊的值**
在剛才的例子中,塊參數以及塊的執行結果都沒有被使用。接下來,我們會定義一個方法,該方法接收兩個整數參數,并對這兩個整數之間的整數做某種處理后進行合計處理,而“某種處理”則由塊指定(代碼清單 11.8)。
**代碼清單 11.8 total.rb**
~~~
1: def total(from, to)
2: result = 0 # 合計值
3: from.upto(to) do |num| # 處理從from 到to 的值
4: if block_given? # 如果有塊的話
5: result += yield(num) # 累加經過塊處理的值
6: else # 如果沒有塊的話
7: result += num # 直接累加
8: end
9: end
10: return result # 返回方法的結果
11: end
12:
13: p total(1, 10) # 從1 到10 的和 => 55
14: p total(1, 10){|num| num ** 2 } # 從1 到10 的2 次冪的和 => 385
~~~
`total` 方法會先使用 `Integer#upto` 方法把 `from` 到 `to` 之間的整數值按照從小到大的順序取出來,然后交給塊處理,最后再將塊處理后的值累加到變量 `result`。程序第 5 行中,對 `yield` 傳遞參數后,參數值就會作為塊變量傳遞到塊中。同時,塊的運行結果也會作為 `yield` 的結果返回。
程序第 4 行的 `block_given?` 方法被用來判斷調用該方法時是否有塊被傳遞給方法,如果有則返回 `true`,反之返回 `false`。如果方法沒有塊,則在程序第 7 行中直接把 `num` 相加。
在本例中,對 `yield` 傳遞 1 個參數,就有 1 個塊變量接收。下面我們來看看對 `yield` 傳遞 0 個、1 個、3 個等多個參數時,對應的塊變量是如何進行接收的(代碼清單 11.9)。
**代碼清單 11.9 block_args_test.rb**
~~~
def block_args_test
yield() # 0 個塊變量
yield(1) # 1 個塊變量
yield(1, 2, 3) # 3 個塊變量
end
puts "通過|a| 接收塊變量"
block_args_test do |a|
p [a]
end
puts
puts "通過|a, b, c| 接收塊變量"
block_args_test do |a, b, c|
p [a, b, c]
end
puts
puts "通過|*a| 接收塊變量"
block_args_test do |*a|
p [a]
end
puts
~~~
> **執行示例**
~~~
> ruby block_args_test.rb
通過|a| 接收塊變量
[nil]
[1]
[1]
通過|a, b, c| 接收塊變量
[nil, nil, nil]
[1, nil, nil]
[1, 2, 3]
通過|*a| 接收塊變量
[[]]
[[1]]
[[1, 2, 3]]
~~~
首先我們注意到,`yield` 參數的個數與塊變量的個數是不一樣的。從 `|a|` 和 `|a, b, c|` 的例子中可以看出,塊變量比較多時,多出來的塊變量值為 `nil`,而塊變量不足時,則不能接收參數值。
最后的通過 `|*a|` 接收的情況是將所有塊變量整合為一個數組來接收。這與定義方法時接收可變參數的情況非常相似。
另外,在第 4 章中介紹的抽取嵌套數組的元素的規則,同樣也適用于塊變量。例如,`Hash#each_with_index` 方法的塊變量有 2 個,并以 `yield([ 鍵 , 值 ], 索引 )` 的形式傳遞。像代碼清單 11.10 那樣,在接收塊變量后,我們就可以把 `[ 鍵 , 值 ]` 部分分別賦值給不同的變量。
**代碼清單 11.10 param_grouping.rb**
~~~
hash = {a: 100, b: 200, c: 300}
hash.each_with_index do |(key, value), index|
p [key, value, index]
end
~~~
> **執行示例**
~~~
> ruby param_grouping.rb
[:a, 100, 0]
[:b, 200, 1]
[:c, 300, 2]
~~~
### **11.3.3 控制塊的執行**
在調用代碼清單 11.8 的 `total` 方法時,如果像下面那樣在中途使用 `break`,`total` 方法的結果會變成什么樣子呢?
~~~
n = total(1, 10) do |num|
if num == 5
break
end
num
end
p n #=> ??
~~~
答案是 `nil`。在塊中使用 `break`,程序會馬上返回到調用塊的地方,因此 `total` 方法中返回計算結果的處理等都會被忽略掉。但作為方法的結果,當我們希望返回某個值的時候,就可以像 `break 0` 這樣指定 `break` 方法的參數,這樣該值就會成為方法的返回值。
此外,如果在塊中使用 `next`,程序就會中斷當前處理,并繼續執行下面的處理。使用 `next` 后,執行塊的 `yield` 會返回,如果 `next` 沒有指定任何參數則返回 `nil`,而如果像 `next 0` 這樣指定了參數,那么該參數值就是返回值。
~~~
n = total(1, 10) do |num|
if num % 2 != 0
next 0
end
num
end
p n #=> 30
~~~
最后,如果在塊中使用 `redo`,程序就會返回到塊的開頭,并按照相同的塊變量再次執行處理。這種情況下,塊的處理結果不會返回給外部,因此需要十分小心 `redo` 的用法,注意不要使程序陷入死循環。
### **11.3.4 將塊封裝為對象**
如前所述,在接收塊的方法中執行塊時,可以使用 `yield` 關鍵字。
而 Ruby 還能把塊當作對象處理。把塊當作對象處理后,就可以在接收塊的方法之外的其他地方執行塊,或者把塊交給其他方法執行。
這種情況下需要用到 `Proc` 對象。`Proc` 對象是能讓塊作為對象在程序中使用的類。定義 `Proc` 對象的典型的方法是,調用 `Proc.new` 方法這個帶塊的方法。在調用 `Proc` 對象的 `call` 方法之前,塊中定義的程序不會被執行。
在代碼清單 11.11 的例子中,定義一個輸出信息的 `Proc` 對象,并調用兩次。這時,程序就會把 `call` 方法的參數作為塊參數來執行塊。
**代碼清單 11.11 proc1.rb**
~~~
hello = Proc.new do |name|
puts "Hello, #{name}."
end
hello.call("World")
hello.call("Ruby")
~~~
> **執行示例**
~~~
> ruby proc1.rb
Hello, World.
Hello, Ruby.
~~~
把塊從一個方法傳給另一個方法時,首先會通過變量將塊作為 `Proc` 對象接收,然后再傳給另一個方法。在方法定義時,如果末尾的參數使用“& 參數名”的形式,Ruby 就會自動把調用方法時傳進來的塊封裝為 `Proc` 對象。
下面,我們將代碼清單 11.8 中塊的接收方法加以改寫,如下所示:
**代碼清單 11.12 total2.rb**
~~~
1: def total2(from, to, &block)
2: result = 0 # 合計值
3: from.upto(to) do |num| # 處理從from 到to 的值
4: if block # 如果有塊的話
5: result += # 累加經過塊處理的值
6: block.call(num)
7: else # 如果沒有塊的話
8: result += num # 直接累加
9: end
10: end
11: return result # 返回方法的結果
12: end
13:
14: p total2(1, 10) # 從1 到10 的和 => 55
15: p total2(1, 10){|num| num ** 2 } # 從1 到10 的2 次冪的和 => 385
~~~
我們在首行的方法定義中定義了 `&block` 參數。像這樣,在變量名前添加 `&` 的參數被稱為 Proc 參數。如果在調用方法時沒有傳遞塊,`Proc` 參數的值就為 `nil`,因此通過這個值就可以判斷出是否有塊被傳入方法中。另外,執行塊的語句不是 `yield`,而是 `block.call(num)`,這一點與之前的例子也不一樣。
在第 7 章中我們提到過方法可以有多個參數,而且定義參數的默認值等時都需要按照一定的順序。而 `Proc` 參數則一定要在所有參數之后,也就是方法中最后一個參數。
將塊封裝為 `Proc` 對象后,我們就可以根據需要隨時調用塊。甚至還可以將其賦值給實例變量,讓別的實例方法去任意調用。
此外,我們也能將 `Proc` 對象作為塊傳給其他方法處理。這時,只需在調用方法時,用“`&Proc` 對象”的形式定義參數就可以了。例如,向 `Array#each` 方法傳遞塊時,可以像代碼清單 11.13 那樣定義。
**代碼清單 11.13 call_each.rb**
~~~
def call_each(ary, &block)
ary.each(&block)
end
call_each [1, 2, 3] do |item|
p item
end
~~~
這樣一來,我們就可以非常方便地把調用 `call_each` 方法時接收到的塊,原封不動地傳給 `ary.each` 方法。
> **執行示例**
~~~
> ruby call_each.rb
1
2
3
~~~
### **11.4 局部變量與塊變量**
塊內部的命名空間與塊外部是共享的。在塊外部定義的局部變量,在塊中也可以繼續使用。而被作為塊變量使用的變量,即使與塊外部的變量同名,Ruby 也會認為它們是兩個不同的變量。請看代碼清單 11.14。
**代碼清單 11.14 local_and_block.rb**
~~~
x = 1 # 初始化x
y = 1 # 初始化y
ary = [1, 2, 3]
ary.each do |x| # 將x 作為塊變量使用
y = x # 將x 賦值給y
end
p [x, y] # 確認x 與y 的值
~~~
> **執行示例**
~~~
> ruby local_and_block.rb
[1, 3]
~~~
在 `ary.each` 方法的塊中,`x` 的值被賦值給了局部變量 `y`。因此,`y` 保留了最后一次調用塊時塊變量 `x` 的值 3。而變量 `x` 的值在調用 `ary.each` 前后并沒有發生改變。
相反,在塊內部定義的變量不能被外部訪問。在剛才的例子中,如果把第 2 行的代碼刪掉,程序就會出錯。
~~~
x = 1 # 初始化 x
#y = 1 # 初始化 y
ary = [1, 2, 3]
ary.each do |x| # 將 x 作為塊變量使用
y = x # 將 x 賦值給y
end
p [x, y] # 引用 y 時會出錯誤(NameError)
~~~
塊中變量的作用域之所以這么設計,是為了通過與塊外部共享局部變量,從而擴展變量的有效范圍。在塊內部給局部變量賦值的時候,要時刻注意它與塊外部的同名變量的關系。大家一定要小心 Ruby 中的這個小陷阱。
塊變量是只能在塊內部使用的變量(塊局部變量),它不能覆蓋外部的局部變量,但 Ruby 為我們提供了定義塊變量以外的塊局部變量的語法。使用在塊變量后使用 `;` 加yiqufen以區分的方式,來定義塊局部變量。這里我們再稍微修改一下剛才的例子,如下所示。可以看出,塊執行后 `x` 和 `y` 的值并沒有變化。
**代碼清單 11.15 local_and_block2.rb**
~~~
x = y = z = 0 # 初始化x、y、z
ary = [1, 2, 3]
ary.each do |x; y| # 使用塊變量x,塊局部變量y
y = x # 代入塊局部變量y
z = x # 代入不是塊局部變量的變量z
p [x, y, z] # 確認塊內的 x、y、z 的值
end
puts
p [x, y, z] # 確認x、y、z 的值
~~~
> **執行示例**
~~~
> ruby local_and_block2.rb
[1, 1, 1]
[2, 2, 2]
[3, 3, 3]
[0, 0, 3]
~~~
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 1 部分 Ruby 初體驗
- 第 1 章 Ruby 初探
- 第 2 章 便利的對象
- 第 3 章 創建命令
- 第 2 部分 Ruby 的基礎
- 第 4 章 對象、變量和常量
- 第 5 章 條件判斷
- 第 6 章 循環
- 第 7 章 方法
- 第 8 章 類和模塊
- 第 9 章 運算符
- 第 10 章 錯誤處理與異常
- 第 11 章 塊
- 第 3 部分 Ruby 的類
- 第 12 章 數值類
- 第 13 章 數組類
- 第 14 章 字符串類
- 第 15 章 散列類
- 第 16 章 正則表達式類
- 第 17 章 IO 類
- 第 18 章 File 類與 Dir 類
- 第 19 章 Encoding 類
- 第 20 章 Time 類與 Date 類
- 第 21 章 Proc 類
- 第 4 部分 動手制作工具
- 第 22 章 文本處理
- 第 23 章 檢索郵政編碼
- 附錄
- 附錄 A Ruby 運行環境的構建
- 附錄 B Ruby 參考集
- 后記
- 謝辭