# **第 21 章 Proc 類**
在本章中,我們將介紹 `Proc` 類相關的內容。
-
**Proc 類是什么**
介紹 `Proc` 類是什么、以及創建 `Proc` 對象的幾種方法。
-
**Proc 對象的特征**
`Proc` 對象具有部分程序的特質,并不是普通的數據,這里將會介紹 `Proc` 類的相關特質。
-
**Proc 類的實例方法**
介紹 `Proc` 類的實例方法。
### **21.1 Proc 類是什么**
所謂 `Proc`,就是使塊對象化的類。`Proc` 與塊的關系非常密切,在第 11 章中我們也介紹過 `Proc` 類。請大家結合第 11 章的內容,一起學習本章。
下面,我們來看看如何創建與執行 `Proc` 對象。
-
**`Proc.new`(...)
`proc`{...}**
創建 `Proc` 對象的典型方法是通過 `Proc.new` 方法,或者對 `proc` 方法指定塊。
~~~
hello1 = Proc.new do |name|
puts "Hello, #{name}."
end
hello2 = proc do |name|
puts "Hello, #{name}."
end
hello1.call("World") #=> Hello, World.
hello2.call("Ruby") #=> Hello, Ruby.
~~~
利用 `Proc.new` 方法,或者對 `proc` 方法指定塊,都可以創建代表塊的 `Proc` 對象。
通過調用 `Proc#call` 方法執行塊。調用 `Proc#call` 方法時的參數會作為塊變量,塊中最后一個表達式的值則為 `Proc#call` 的返回值。`Proc#call` 還有一個名稱叫 `Proc#[]`。
~~~
# 判斷西歷的年是否為閏年的處理
leap = Proc.new do |year|
year % 4 == 0 && year % 100 != 0 || year % 400 ==0
end
p leap.call(2000) #=> true
p leap[2013] #=> false
p leap[2016] #=> true
~~~
將塊變量設置為 |* 數組 | 的形式后,就可以像方法參數一樣,以數組的形式接收可變數量的參數。
~~~
double = Proc.new do |*args|
args.map{|i| i * 2 } # 所有元素乘兩倍
end
p double.call(1, 2, 3) #=> [2, 3, 4]
p double[2, 3, 4] #=> [4, 6, 8]
~~~
除此以外,定義普通方法時可使用的參數形式,如默認參數、關鍵字參數等,幾乎都可以被用于塊變量的定義,并被指定給 `Proc#call` 方法。關于方法定義的參數指定,請參考第 7 章。
### **21.1.1 lambda**
`Proc.new`、`proc` 等有另外一種寫法叫 `lambda`。與 `Proc.new`、`proc` 一樣,`lambda` 也可以創建 `Proc` 對象,但通過 `lambda` 創建的 `Proc` 的行為會更接近方法。
第一個不同點是,`lambda` 的參數數量的檢查更加嚴密。對用 `Proc.new` 創建的 `Proc` 對象調用 `call` 方法時,`call` 方法的參數數量與塊變量的數量可以不同。但通過 `lambda` 創建 `Proc` 對象時,如果參數數量不正確,程序就會產生錯誤。
~~~
prc1 = Proc.new do |a, b, c|
p [a, b, c]
end
prc1.call(1, 2) #=> [1, 2, nil]
prc2 = lambda do |a, b, c|
p [a, b, c]
end
prc2.call(1, 2) #=> 錯誤(ArgumentError)
~~~
第二個不同點是,`lambda` 可以使用 `return` 將值從塊中返回。請看代碼清單 21.1。`power_of` 方法會利用參數 `n` 返回“計算 x 的 n 次冪的 Proc 對象”。請注意,返回值并不是數值,而是進行運算的 `Proc` 對象。調用 `power_of(3)` 后,結果就會得到 `call` 方法參數值的 3 次冪的 `Proc` 對象。從 `lambda` 中返回值時使用了 `return`,這里的 `return` 會將 `lambda` 中的值返回。
**代碼清單 21.1 power_of.rb**
~~~
def power_of(n)
lambda do |x|
return x ** n
end
end
cube = power_of(3)
p cube.call(5) #=> 125
~~~
接下來,我們嘗試用 `Proc.new` 方法改寫代碼清單 21.1。使用 `Proc.new` 方法時,在塊中使用 `return` 后,程序就會跳過當前執行塊,直接從創建這個塊的方法返回。在本例中,即雖然塊內的 `return` 應該從 `power_of` 方法返回,但由于程序運行時 `power_of` 方法的上下文會消失,因此程序就會出現錯誤。
~~~
def power_of(n)
Proc.new do |x|
return x ** n
end
end
cube = power_of(3)
p cube.call(5) #=> 錯誤(LocalJumpError)
~~~
不是 `lambda` 的普通塊中的 `return`,會從正在執行循環的方法返回。代碼清單 21.2 中的 `prefix` 方法會比較參數 `ary` 中的元素是否與 `obj` 相等,相等就返回在此之前的所有元素,不相等則返回空數組。第 6 行中的 `return` 并不會從塊返回,而是跳過塊,并作為 `prefix` 方法整體的返回值返回。
**代碼清單 21.2 prefix.rb**
~~~
1: def prefix(ary, obj)
2: result = [] # 初始化結果數組
3: ary.each do |item| # 逐個檢查元素
4: result << item # 將元素追加到結果數組中
5: if item == obj # 如果元素與條件一致
6: return result # 返回結果數組
7: end
8: end
9: return result # 所有元素檢查完畢的時候
10: end
11:
12: prefix([1, 2, 3, 4, 5], 3) #=> [1, 2, 3]
~~~
`break` 被用于控制迭代器的行為。這個命令會向接收塊的方法的調用者返回結果值。如下所示,`break []` 會馬上終止 `Array#collect` 方法,并將空數組作為 `collent` 方法的整體的返回值返回。
~~~
[:a, :b, :c].collect do |item|
break []
end
~~~
> **注** 用 `Proc.new` 方法或者 `proc` 方法創建的 `Proc` 對象的情況下,由于這些方法都接收塊,在調用 `Proc#call` 方法的時候并沒有適當的返回對象,因此就會發生錯誤。而 `lambda` 的情況下則與 `return` 一樣,將值返回給 `Proc#call` 方法。另一方面,由于 `next` 方法的作用在于中斷 1 次塊的執行,因此無論如何創建 `Proc` 對象,都可以將值返回給 `call` 方法。
`lambda` 有另外一種寫法——“`->( 塊變量 ){ 處理 }`”。塊變量在 `{ ~ }` 之前,看上去有點像函數。使用 `->` 的時候,我們一般會使用 `{ ~ }` 而不是 `do ~ end`。
~~~
square = ->(n){ return n ** 2}
p square[5] #=> 25
~~~
### **21.1.2 通過 Proc 參數接收塊**
在調用帶塊的方法時,通過 `Proc` 參數的形式指定塊后,該塊就會作為 `Proc` 對象被方法接收。代碼清單 21.3 是我們在第 11 章中介紹過的例子。在 `total2` 方法中,調用 `total2` 方法時指定的塊,可以作為 `Proc` 對象從變量 `block` 中獲取。
**代碼清單 21.3 total2.rb**
~~~
def total2(from, to, &block)
result = 0 # 合計值
from.upto(to) do |num| # 處理從 from 到 to 的值
if block # 如果有塊的話
result += # 累加經過塊處理的值
block.call(num)
else # 如果沒有塊的話
result += num # 直接累加
end
end
return result # 返回方法的結果
end
p total2(1, 10) # 從 1 到 10 的和 => 55
p total2(1, 10){|num| num ** 2 } # 從 1 到 10 的 2 次冥的和 => 385
~~~
### **21.1.3 to_proc 方法**
有些對象有 `to_proc` 方法。在方法中指定塊時,如果以 & 對象的形式傳遞參數,對象 `.to_proc` 就會被自動調用,進而生成 `Proc` 對象。
其中,`Symbol#to_proc` 方法是比較典型的,并且經常被用到。例如,對符號 `:to_i` 使用 `Symbol#to_proc` 方法,就會生成下面那樣的 `Proc` 對象。
~~~
Proc.new{|arg| arg.to_i }
~~~
這個對象在什么時候使用呢?例如,把數組的所有元素轉換為數值類型時,一般的做法如下:
> **執行示例**
~~~
>> %w(42 39 56).map{|i| i.to_i }
=> [42, 39, 56]
~~~
上述代碼還可以像下面這樣寫:
> **執行示例**
~~~
>> %w(42 39 56).map(&:to_i)
=> [42, 39, 56]
~~~
按照類名排序的程序,也可以寫成:
> **執行示例**
~~~
>> [Integer, String, Array, Hash, File, IO].sort_by(&:name)
=> [Array, File, Hash, IO, Integer, String]
~~~
熟悉這樣的寫法可能需要一定的時間,但這種寫法不僅干凈利索,而且意圖明確。
### **21.2 Proc 的特征**
雖然 `Proc` 對象可以作為匿名函數或方法使用,但它并不只是單純的對象化。請看代碼清單 21.4。
**代碼清單 21.4 counter_proc.rb**
~~~
1: def counter
2: c = 0 # 初始化計數器
3: Proc.new do # 每調用 1 次 call 方法,計數器加1
4: c += 1 # 返回加 1 后的 Proc 對象
5: end
6: end
7:
8: # 創建計數器 c1 并計數
9: c1 = counter
10: p c1.call #=> 1
11: p c1.call #=> 2
12: p c1.call #=> 3
13:
14: # 創建計數器 c2 并計數
15: c2 = counter # 創建計數器c2
16: p c2.call #=> 1
17: p c2.call #=> 2
18:
19: # 再次用 c1 計數
20: p c1.call #=> 4
~~~
第 1 行到第 6 行為 `counter` 方法的定義。該方法首先把作為計數器的本地變量 `c` 初始化為 0。然后每調用 1 次 `Proc#call` 方法,就將計數器加 1,并返回該 `Proc` 對象。在第 9 行中,調用 `counter` 方法,將 `Proc` 對象賦值給 `c1`。可以看到,`c1` 調用 `call` 方法后,`proc` 對象引用的本地變量 `c` 開始計數了。在第 15 行中,以同樣的方法創建新的計數器,之后計數器被重置。在最后的第 20 行中,再次調用最初創建的 `c1` 的 `call` 方法,計數器開始接著之前的結果計數。
通過這個例子我們可以看出,變量 `c1` 與變量 `c2` 引用的 `Proc` 對象,是分別保存、處理調用 `counter` 方法時初始化的本地變量的。與此同時,`Proc` 對象也會將處理內容、本地變量的作用域等定義塊時的狀態一起保存。
像 `Proc` 對象這樣,將處理內容、變量等環境同時進行保存的對象,在編程語言中稱為閉包(closure)。使用閉包后,程序就可以將處理內容和數據作為對象來操作。這和在類中描述處理本身、在實例中保存數據本質上是一樣的,只是從寫程序的角度來看,使用類的話當然也就意味著可以使用更多的功能。
就像剛才的計數器的例子那樣,`Proc` 對象可被用來對少量代碼實現的功能做對象化處理。另外,由于 Ruby 中大量使用了塊,因此在有一定規模的程序開發中,我們就難免會使用到 `Proc` 對象。特別是像調用和傳遞帶塊的方法時的方法、通過閉包保存數據等功能,我們都需要透徹理解才行。
### **21.3 Proc 類的實例方法**
-
***prc*.`call`(*args, ...*)
*prc*[*args, ...*]
*prc*.`yield`(*args, ...*)
*prc*.(*args, ...*)
*prc* === *arg***
上述方法都執行 `Proc` 對象 *prc*。
~~~
prc = Proc.new{|a, b| a + b}
p prc.call(1, 2) #=> 3
p prc[3, 4] #=> 7
p prc.yield(5, 6) #=> 11
p prc.(7, 8) #=> 15
p prc === [9, 10] #=> 19
~~~
由于受到語法的限制,通過 `===` 指定的參數只能為 1 個。大家一定要牢記這個方法會在 `Proc` 對象作為 `case` 語句的條件時使用。因此,在創建這樣的 `Proc` 對象時,比較恰當的做法是,只接收一個參數,并返回 `true` 或者 `false`。
下面的例子實現的是,從 1 到 100 的整數中,當值為 3 的倍數時輸出 `Fizz`,5 的倍數時輸出 `Buzz`,15 的倍數時輸出 `Fizz Buzz`,除此以外的情況下則輸出該值本身。
~~~
fizz = proc{|n| n % 3 == 0 }
buzz = proc{|n| n % 5 == 0 }
fizzbuzz = proc{|n| n % 3 == 0 && n % 5 == 0}
(1..100).each do |i|
case i
when fizzbuzz then puts "Fizz Buzz"
when fizz then puts "Fizz"
when buzz then puts "Buzz"
else puts i
end
end
~~~
-
***prc*.`arity`**
返回作為 `call` 方法的參數的塊變量的個數。以 `|*args|` 的形式指定塊變量時,返回 -1。
~~~
prc0 = Proc.new{ nil }
prc1 = Proc.new{|a| a }
prc2 = Proc.new{|a, b| a + b }
prc3 = Proc.new{|a, b, c| a + b +c }
prcn = Proc.new{|*args| args }
p prc0.arity #=> 0
p prc1.arity #=> 1
p prc2.arity #=> 2
p prc3.arity #=> 3
p prcn.arity #=> -1
~~~
-
***prc*.`parameters`**
返回關于塊變量的詳細信息。返回值為 [ 種類 , 變量名 ] 形式的數組的列表。表 21.1 為表示種類的符號。
**表 21.1 Proc#parameters 返回的變量種類**
| 符號 | 意義 |
|-----|-----|
| `:opt` | 可省略的變量 |
| `:req` | 必需的變量 |
| `:rest` | 以 \*_args_ 形式表示的變量 |
| `:key` | 關鍵字參數形式的變量 |
| `:keyrest` | 以 \*\*_args_ 形式表示的變量 |
| `:block` | 塊 |
~~~
prc0 = proc{ nil }
prc1 = proc{|a| a }
prc2 = lambda{|a, b| [a, b] }
prc3 = lambda{|a, b=1, *c| [a, b, c] }
prc4 = lambda{|a, &block| [a, block] }
prc5 = lambda{|a: 1, **b| [a, b] }
p prc0.parameters #=> []
p prc1.parameters #=> [[:opt, :a]]
p prc2.parameters #=> [[:req, :a], [:req, :b]]
p prc3.parameters #=> [[:req, :a], [:opt, :b], [:rest, :c]]
p prc4.parameters #=> [[:req, :a], [:block, :block]]
p prc5.parameters #=> [[:key, :a], [:keyrest, :b]]
~~~
-
***prc*.`lambda?`**
判斷 *prc* 是否為通過 `lambda` 定義的方法。
~~~
prc1 = Proc.new{|a, b| a + b}
p prc1.lambda? #=> false
prc2 = lambda{|a, b| a + b}
p prc2.lambda? #=> true
~~~
-
***prc*.`source_location`**
返回定義 *prc* 的程序代碼的位置。返回值為 [ 代碼文件名 , 行編號 ] 形式的數組。*prc* 由擴展庫等生成,當 Ruby 腳本不存在時返回 `nil`。
**代碼清單 21.5 proc_source_location.rb**
~~~
1: prc0 = Proc.new{ nil }
2: prc1 = Proc.new{|a| a }
3:
4: p prc0.source_location
5: p prc1.source_location
~~~
> **執行示例**
~~~
> ruby proc_source_location.rb
["proc_source_location.rb", 1]
["proc_source_location.rb", 2]
~~~
### **練習題**
1. 仿照 `Array#collect` 方法,定義 `my_collect` 方法。參數為擁有 `each` 方法的對象,并在塊中對各元素進行處理。
~~~
def my_collect(obj, &block)
(??)
end
ary = my_collect([1, 2, 3, 4, 5]) do |i|
i * 2
end
p ary #=> [2, 4, 6, 8, 10]
~~~
2. 確認使用了下述 `Symbol#to_proc` 方法的例子的執行結果。
~~~
to_class = :class.to_proc
p to_class.call("test") #=> ??
p to_class.call(123) #=> ??
p to_class.call(2 ** 1000) #=> ??
~~~
3. 修改計數器的例子,計算 `call` 方法的參數的合計值。請補充下面 (??) 部分的代碼。
~~~
def accumlator
total = 0
Proc.new do
(??)
end
end
acc = accumlator
p acc.call(1) #=> 1
p acc.call(2) #=> 3
p acc.call(3) #=> 6
p acc.call(4) #=> 10
~~~
> 參考答案:請到圖靈社區本書的“隨書下載”處下載([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 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 參考集
- 后記
- 謝辭