在 Ruby 中容易搞混(學不好),且面試經常會被問的問題:
「請說明 Block、Proc、Lambda 是什麼」
「Block 中的 do..end 與 花括號 { } 差異」
「請說明 Proc 與 Lambda 區別」
「Rails 的 scope 為什麼用 Lambda?」
「要怎麼把 Block 轉成 Proc、Lambda?」
「要怎麼把 Proc、Lambda 轉成 Block?」
上述常見問題,一次滿足!!
Block (程式碼區塊)
什麼是 Block ?
Ruby 是一款相當徹底「物件導向 OOP (Object-Oriented Programming)」的程式語言,絕大部分的東西都是物件,而 Block 是少數的例外。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58list = [1, 3, 5, 7, 9, 10, 12]
list.map { |i| i * 2 }
# map 印出如下
2
6
10
14
18
20
24
=> [2, 6, 10, 14, 18, 20, 24] # 回傳值
# map 印出如上
list.select { |j| p j.even? }
# select 印出如下
false
false
false
false
false
true
true
=> [10, 12] # 回傳值
# select 印出如上
list.reduce { |x, y| p x + y }
# reduce 印出如下
4
9
16
25
35
47
=> 47 # 回傳值
# reduce 印出如上
list.each do |num|
p num * 2
end
# each 印出如下
2
6
10
14
18
20
24
=> [1, 3, 5, 7, 9, 10, 12] # 回傳值
# each 印出如上
# 從上得知,map、select、reduce 與 each 的回傳值(return value)不同
# map、select、reduce 會回傳一個新陣列
# each 回傳 receiver
在 Ruby 中,花括號 {}
與 do..end
就是 Block,要接在方法(method)後面,且無法單獨存活,否則會出錯。
1
2
3
4
5
6
7
8
9{ puts "無法單獨存活,會出錯" } # SyntaxError
do
puts "無法單獨存活,會出錯" # SyntaxError
end
# Block 無法單獨存活,會噴錯誤訊息
Block 不是物件,不能單獨存在。
Block 只是附加在方法後面,等著被程式碼呼叫的一段程式碼。
Block 中 花括號 {}
與 do..end
差異
一般來說,若可以一行寫完會使用 {}
,若遇上較複雜的判斷,需寫一行以上時,則會使用 do..end
。
除此之外,還有別的差異嗎?
請看以下範例:
1
2
3
4
5list = [1, 2, 3, 4, 5]
p list.map{ |x| x * 2 } # [2, 4, 6, 8, 10]
p list.map do |x| x * 2 end # <Enumerator: [1, 2, 3, 4, 5]:map>
原來 花括號 {}
與 do..end
還有 優先順序
的不同
花括號 {}
優先順序大於do..end
上述範例原形為
1
2
3
4
5
6list = [1, 2, 3, 4, 5]
p(list.map{ |x| x * 2 } # [2, 4, 6, 8, 10]
p(list.map) do |x| x * 2 end # <Enumerator: [1, 2, 3, 4, 5]:map>
# 因為優先順序較低,所以變成先跟 p 結合了,造成後面附掛的 Block 就不會被處理了
如何執行 Block 的內容?
使用
yield
方法
如果想讓附掛的 Block 執行內容,可以使用 yield
方法,能暫時先把控制權交給 Block ,等 Block 執行結束後再把控制權交回來。
1
2
3
4
5
6
7
8
9
10
11
12
13def hi_block
puts "開始"
yield # 把控制權暫時讓給 Block
puts "結束"
end
hi_block { puts "這裡是 Block" }
# 印出結果如下
開始
這裡是 Block
結束
=> nil
# 印出結果如上
傳參數給 Block
會發現不管在 list.map { |i| i * 2 }
或 list.each do |num| p num * 2 end
的 Block 中,那個 |i|
和 |num|
是什麼?
Block 中包住 i
和 num
的 | 唸做 pipe,中間的 i
與 num
是匿名函數的參數,稱作 token
,其實是 Block 範圍裡的區域變數,離開 Block 之後就會失效了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15list = [1, 3, 5, 7, 9, 10, 12]
list.map { |i| i * 2 }
# 變數 i 只有在 Block 裡有效,會產生 [2, 6, 10, 14, 18, 20, 24]
list.each do |num|
p num * 2
end
# 變數 num 只有在 Block 裡有效,會依序印出 2、6、10、14、18、20、24
puts i # 離開 Block 之後就失效,出現找不到變數的錯誤 (NameError)
puts num # 離開 Block 之後就失效,出現找不到變數的錯誤 (NameError)
# 變數名稱自定義,會取有意義的名稱,讓人看到能知道是什麼,而不是取無意義的 i
所以, i
和 num
是怎麼來的?
事實上,它就只是你在使用 yield
方法把控制權轉讓給 Block 的時候,順便把值帶給 Block 而已。
1
2
3
4
5
6
7
8
9
10
11
12
13
14def hi_block
puts "開始"
yield "媽,我在這" # 也可寫 yield("媽,我在這")
puts "結束"
end
hi_block { |x| puts "這裡是 Block,#{x}" }
# 印出結果如下
開始
這裡是 Block,媽,我在這
結束
=> nil
# 印出結果如上
yield
後面可以帶 1 個或以上的參數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30# 範例1 (帶 1 個參數)
def hi_block
puts "開始"
yield 123 # 把控制權暫時讓給 Block,並且傳數字 123 給 Block
puts "結束"
end
hi_block { |x| # 這個 x 是來自 yield 方法
puts "這裡是 Block,我收到了 #{x}"
}
# 印出結果如下
開始
這裡是 Block,我收到了 123
結束
=> nil
# 印出結果如上
# 範例2 (帶 2 個參數)
def tow_parm
yield(123, "參數2")
end
tow_parm { |m, n|
puts %Q(我說數字 #{m},你回#{n}~)
}
# 印出結果如下
我說數字 123,你回參數2~
=> nil
# 印出結果如上
yield
進階使用
範例 1:
第 7 行的 i
被 yield
出去找了第 11 行的 i
, x
是實體變數 @v
用 each
方法印出陣列中的數字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Map
def initialize
@v = [1, 2, 3, 4]
end
def each_print
@v.each { |i| puts yield i } if block_given?
end
end
i = "多看幾次就會理解了"
a_obj = Map.new
a_obj.each_print{ |x| "#{i} #{x}" }
# 印出結果如下
多看幾次就會理解了 1
多看幾次就會理解了 2
多看幾次就會理解了 3
多看幾次就會理解了 4
=> [1, 2, 3, 4]
# 印出結果如上
範例 2:
第 4 行的 yield
將 counter 帶去 method 外面找 list 後面的 Block,因 first 預設為 1 ,得知 yield
後面的 counter 預設也為 1,成為外面 Block 中 |ary|
的參數,待 Block 執行完後再回到第 4 行繼續往下執行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32def list(array, first = 1)
counter = first
array.each do |item|
puts "#{yield counter}. #{item}"
counter = counter.next
end
end
list(["a","b","c"]) { |ary| ary * 3 }
# 印出結果如下
3. a
6. b
9. c
=> ["a", "b", "c"]
# 印出結果如上
list(["a","b","c"], 100) { |ary| ary * 3 }
# 印出結果如下
300. a
303. b
306. c
=> ["a", "b", "c"]
# 印出結果如上
list(["Ruby", "Is", "Fun"], "A") { |ary| ary * 3}
# 印出結果如下
AAA. Ruby
BBB. Is
CCC. Fun
=> ["Ruby", "Is", "Fun"]
# 印出結果如上
Block 的回傳值
其實 yield
方法除了把控制權暫時的讓給後面的 Block 之外
Block 最後一行的執行結果也會自動變成 Block 的回傳值
所以可把 Block 當做判斷內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 範例1
def dog
puts "汪!!汪!!汪!!"
end
dog { puts "你看不見我~~" } # 汪!!汪!!汪!!
# 如果沒有 yield,寫在 Block 裡面的東西,是不會有反應的
# 範例2
def say_hello(list)
result = []
list.each do |i|
result << i if yield(i) # 如果 yield 的回傳值是 true 的話...
end
result
end
p say_hello([*1..10]) { |x| x % 2 == 0 } # [2, 4, 6, 8, 10]
p say_hello([*1..10]) { |x| x < 5 } # [1, 2, 3, 4]
p say_hello([*1..10]) { |x| return x < 5 } # 會產生 LocalJumpError 的錯誤
p say_hello([*1..10]) # 會產生 LocalJumpError (no block given (yield)) 錯誤訊息
上述範例 say_hello
方法,會根據 Block 的設定條件,挑出符合條件的元素,需特別留意在 Block 裡加入 return
會造成 LocalJumpError
的錯誤,因為 Block 不是一個方法,它不知道你要 return
到哪裡去而造成錯誤。
Block 不是參數
Block 像寄生蟲一樣得依附或寄生在其他的方法或物件,但它不是參數,以下範例中, name 才是參數,但 Block 不是。
1
2
3
4
5def say_hello(name)
p name
end
say_hello("小菜") { puts "這是 Block"} # "小菜"
上面這段程式碼執行後不會有任何錯誤,但 Block 裡面也不會被執行。
怎判斷有無 Block?
有一種狀況是方法裡有 yield
,但是呼叫方法的時候卻沒有 Block 的話...
1
2
3
4
5def say_hello
yield
end
say_hello # 會產生 LocalJumpError (no block given (yield)) 錯誤訊息
會出現 LocalJumpError (no block given (yield))
的錯誤訊息。
在這種狀況,要讓方法呼叫的時候也能正常執行
可以使用 Ruby 提供的一個判斷方法 block_given?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# 範例1
def hi_block
if block_given? # 判斷執行方法的後面有沒有跟 Block
yield
else
"no block"
end
# 上面 5 行可簡寫成 block_given? ? yield : "no block"
end
hi_block # "no block"
hi_block { "hello" } # "hello"
hi_block do "hello" end # "hello"
# 範例2
def hello_world
yield('小菜') if block_given? # 判斷執行方法的後面有沒有跟 Block
# 也可寫 yield '小菜' if block_given?
end
p hello_world # nil
hello_world {|x| puts "#{x}" } # 小菜
# 範例3
def say_hello(name)
yield name if block_given? # 判斷執行方法的後面有沒有跟 Block
end
say_hello(puts "hi") # hi
Block 特性
總結上述所說特性
- 不是物件、不是參數
- 不能單獨存在,只是附加在方法後面,等著被程式碼呼叫的一段程式碼。
- Block 最後一行的執行結果也會自動變成 Block 的回傳值
- Block 內不能使用 return
- 不能賦值給其他物件
雖然 Block 不是物件,不能單獨存在
但 Ruby 有內建兩個方法使 Block 物件化且單獨存在: Proc 和 Lamda
Proc
Proc 是程序物件,可以將 Ruby 的程式碼保存起來,並且在需要的時候再執行它,或當作 Block 傳入其他函數。
在 Proc.new
後面接一個 Block 就可以產生一個 Proc 的物件,物件化後就是一個參數,接著可以使用 call
方法來執行 Block 內的程式碼。
1
2
3
4
5
6
7
8proc1 = Proc.new { puts "Block 被物件化囉" } # 使用 Proc 類別可把 Block 物件化
# 也可以寫成
proc2 = Proc.new do
puts "Block 被物件化囉"
end
proc1.call # Block 被物件化囉
proc2.call # Block 被物件化囉
Proc 中不能加入 return
return
不要寫在 Proc 的 Block 裡,否則程式碼執行到這段後就會停止(return 完後立即結束執行),程式碼不會繼續往下走。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def hi_proc
p "strat"
hi_proc = Proc.new { return "執行完這段就停止了" }
hi_proc.call
p "end"
end
p hi_proc
# 顯示結果如下
"strat"
"執行完這段就停止了"
=> "執行完這段就停止了"
# 顯示結果如上
# "end"不會印出,因為執行完第 3 行就停止了
Proc 可帶參數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# 範例1
hi_river = Proc.new { |name| puts "你好,#{name}"}
# 也可寫成 hi_river = proc { |name| puts "你好,#{name}" }
hi_river.call("小菜在這裡") # 你好,小菜在這裡
# 範例2 (帶參數)
cal = Proc.new { |num| num * 5 }
# 也可寫成 cal = proc { |num| num * 5 }
cal.call(3) # 15
# 範例3 (帶參數)
def total_price(price)
Proc.new { |num| num * price }
# 也可寫成 proc { |num| num * price }
end
n1 = total_price(50)
n2 = total_price(30)
puts "n1 要 #{n1.call(2)} 元,而 n2 要 #{n2.call(5)} 元"
# n1 要 100 元,而 n2 要 150 元
Proc 呼叫方式
要執行一個 Proc 物件,除了 call
方法之外,還有以下幾種使用方法:
1
2
3
4
5
6
7
8
9
10
11hi_river = Proc.new { |name| puts "你好,#{name}"}
hi_river.call("小菜在這裡") # 使用 call 方法
hi_river.("小菜在這裡") # 使用小括號(注意,方法後面有多一個小數點)
hi_river["小菜在這裡"] # 使用中括號
hi_river === "小菜在這裡" # 使用三個等號
hi_river.yield "小菜在這裡" # 使用 yield 方法
# 上述 5 種方法皆印出
# 你好,小菜在這裡
Lambda
Block 除了能轉成 Proc,Block 也可以轉成 Lambda,與 Proc 有些微不同:
retrun
值- 參數的判斷方式 (是否會檢查參數的數量正確性)
Proc、Lambda 怎麼分
1
2
3
4
5
6
7
8
9
10p1 = Proc.new {|x| x + 1 }
p2 = proc {|x| x + 1 } # Proc 的另種寫法
l1 = lambda {|x| x + 1 }
l2 = ->(x) { x + 1 } # lambda 的另種寫法
puts "p1: #{p1.lambda?}, #{p1.class}" # p1: false, Proc
puts "p2: #{p2.lambda?}, #{p2.class}" # p2: false, Proc
puts "l1: #{l1.lambda?}, #{l1.class}" # l1: true, Proc
puts "l2: #{l2.lambda?}, #{l2.class}" # l2: true, Proc
現在我們知道
Proc 和 Lambda 一樣都是屬於 Proc 物件
上面 p1
、p2
、l1
、l2
都可以使用 call
方法來執行,其中我們可以用 lambda?
來判斷它是不是 Lambda,如果不是那它就是 Proc。
Lambda 裡可加入 return
Lambda 與 Proc 的其中一個差異是 return
值不一樣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def hi_lambda
p "strat"
hi_lambda = lambda { return p "會繼續往下執行" }
hi_lambda.call
p "end"
end
p hi_lambda
# 顯示結果如下
"strat"
"會繼續往下執行"
"end"
"end"
=> "end"
# 顯示結果如上
一次比較 Proc 和 Lambda 的 return 值
1
2
3
4
5
6
7
8
9
10def test_return(callable_object)
callable_object.call * 5
end
la = lambda { return 10 } # 也可寫成 la = ->{ return 10 }
pr = proc { return 10 } # 也可寫成 pr = Proc.new { return 10 }
puts test_return(la) # 50
puts test_return(pr) # 顯示 LocalJumpError 錯誤訊息
Lambda 的 return 是從 Lambda return
Proc 則是從定義 Proc 的 scope return
講得很清楚,聽得很模糊嗎? 直接看 code 理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14def test_proc
pr = Proc.new { return 10 }
result = pr.call
return result * 5
end
def test_lambda
la = lambda { return 10 }
result = la.call
return result * 5
end
puts test_proc # 10
puts test_lambda # 50
test_proc
在 pr.call
那行就 retrun 結束執行了,而 test_lambda
可以執行完方法中的每行程式。
Lambda 處理參數較嚴謹
Proc 處理參數較有彈性,而 Lambda 較嚴謹
1
2
3
4
5
6
7
8
9
10
11
12
13 pr = proc { |a, b| [a, b] } # 也可寫成 pr = Proc.new{ |a, b| [a, b] }
la = lambda { |a, b| [a, b] } # 也可寫成 la = ->(a, b){ [a, b] }
p pr.call(5, 6) # [5, 6]
p pr.call # [nil, nil]
p pr.call(5) # [5, nil]
p pr.call(5, 6, 7) # [5, 6]
p la.call(5, 6) # [5, 6]
p la.call # 顯示 ArgumentError 錯誤訊息
p la.call(5) # 顯示 ArgumentError 錯誤訊息
p la.call(5, 6, 7) # 顯示 ArgumentError 錯誤訊息
Proc 針對參數的數量不會進行檢查,不足補 nil ,過多會自動丟掉;Lambda 會要求參數數量正確才會執行,較嚴謹,否則會顯示 ArgumentError 錯誤訊息。
Rails 的 scope 為什麼用 Lambda?
假設我們寫會帶入參數的 scope
1
scope :product_price, -> (type) { where(price: type) }
以 Proc 來做的話,Prodct.product_price
沒帶參數時,SQL query 依舊能夠執行,不會噴錯,因為 Proc 會將沒帶入的參數值預設為 nil ,在 SQL query 等同於執行 where(price: nil)
,會出現你預料外的狀況,在 Debug 會比較不好找。
反而 Lambda 能夠確保參數的數量正確性,過多或太少皆會 error 告訴你不能這麼做,避免不必要的狀況。
這也就是為什麼 Rails 中的 ActiveRecord
model 在使用 scope 時,會用 Lambda 進行傳遞,原因是相比 Proc 來說,更為謹慎。
反而 Lambda 表現更像是常見的匿名函數。
使用 &
符號將 Block 與 Proc、Lambda 轉換
Block 轉成 Proc、Lambda
在 Rails 當中,假如我們要從資料庫找出所有使用者的姓名,利用 map 的話,寫法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# Block 轉 Proc 範例
names = User.all.map { |user| user[:name] }
# 組成一個全都是姓名的 Array
# 也可寫成
names = User.all.map(&:name)
# 將 Block 轉 Proc
# Block 轉 Proc 範例
pp = Proc.new { |x| puts x * 2 }
[1, 2, 3].each(&pp)
# 原形 [1, 2, 3].each{ |i| pp[i] }
# 印出結果如下
2
4
6
=> [1, 2, 3]
# 印出結果如上
# Block 轉 Lambda 範例
lam = lambda { |x| puts x * 2 }
[1,2,3].each(&lam)
# 原形 [1, 2, 3].each{ |i| lam[i] }
# 印出結果如下
2
4
6
=> [1, 2, 3]
# 印出結果如上
那個奇怪的 &
符號代表帶入一個 Proc 或 lambda,將 Block 轉成 Proc 使用。
Proc 或 lambda 轉成 Block
剛才介紹 &
的其中一個用法,那就是在方法宣告同時,指定從 Block 轉成 Proc 或 Lambda,除此 &
還可以把 Proc 或 Lambda 轉成 Block:
1
2
3hi_proc("Hahaha", &proc{ |s| puts s} )
hi_lambda = (1..5).map &->(x){ x*x }
當 Proc 或 Lambda 碰到 &
之後,會轉換成 Block,所以以上的示範意義與下相同:
1
2
3hi_proc("Hahaha"){ |s| puts s }
hi_lambda = (1..5).map { |x| x * x }
&block
放參數最後面
Block 無法得知被物件化(參數化)後的 Block,需在最後一個參數前面加上 &
,這東西只能有一個,且必須放在最後面,否則會出現 syntax error。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# 錯誤示範
def hi_block(&p, n)
...
end
def hi_block(n, &p1, &p2)
...
end
# 範例1
def hi_block1(str, &test01)
"#{str} #{test01.call(18)}"
end
hi_block1("Hello") { |age| "I'm #{age} years old." }
# "Hello I'm 18 years old."
# 範例2
def temp_b1
yield("參數1") # 小括號可省略
end
def temp_b2(&block)
block.call("參數2")
end
block1 = Proc.new {|x| puts "這是 Proc #{x}"}
block2 = lambda {|x| puts "這是 lambda #{x}"}
temp_b1 { |x| puts "block0 #{x}" } # block0 參數1
temp_b1(&block1) # 這是 Proc 參數1
temp_b2(&block2) # 這是 lambda 參數2
看完會發現 Ruby 中的 &
非常的神奇,背後做了很多事情,實際上它是生出一個 Proc 物件,雖然好用,但若不了解背後原理的話,會不知怎麼用、錯在哪。
有兩個以上 &block
該怎辦?
1
2
3
4
5
6
7
8
9
10
11def two_block(n, p1, p2)
p1[n] # 等同於 p1.call(n)
p2.call n # 括號可省略
end
two_block('River', proc { |i| puts "#{i} 1" }, Proc.new { |i| puts "#{i} 2" } )
# 印出結果如下
River 1
River 2
=> nil
# 印出結果如上
建立一個 Proc 物件,並當參數傳入即可,但還是得在建立同時寫 Block 給 Proc.new 方法。乍看之下很冗長又不好看,當想同時傳入多個 Block 作為參數時,適用此技。
小結
這篇很燒腦,找蠻多資料參考,從一開始撰寫時不太清楚,到後來能解釋,過程中有感覺變強一些。
寫不清楚或錯誤部分,歡迎提出討論。