"Blocks are powerful tool for controlling scope, meaning which variables and methods can be seen by which lines of code." from Metaprogramming Ruby
Blocks 應該是大家都很熟悉的主題,因為不管是初學者還是資深的工程師都很難不寫到 blocks。我對 blocks 剛開始的印象其實就是一段程式碼區塊,在 Ruby 編碼上就是 do…end 及 {… } 如此而已,但以書中的介紹來看,blocks 是非常強大的工具在控管變數、方法所影響的範圍。
Blocks 有兩種寫法:
(1) { ... }: (通常的慣例是單行的程式碼區塊就會以 { … } 表示)
# block 裡面的|num| 就是在程式碼中利用 yeild 方法傳遞過來的值
[1,2,3,4,5].select { |num| num.even? } # => [2, 4]
(2) do … end: (超過一行的程式碼區塊就會以 do … end 表示)
Products.each do |product|
if...
else ...
end
end
ㄧ個 Block 必須依附在 Method 後面,也就是說只有當你呼叫方法的時候,block 才會被執行。
就如以下程式碼來說,只有當使用 yield 時,方法才會開始啟動 block ,否則 block 就是只是ㄧ段不會被執行的程式碼而已。
def a_method(a, b)
a + yield(a, b)
end
a_method(1, 2) { |x, y| (x + y) * 3 } # => 10
判斷有沒有 block 附屬在方法上可以使用 block_given? 來詢問
def a_method
return yield if block_given?
"no block"
end
a_method # => "no block"
a_method { "you got a block" } # => "you got a block"
Block 在其他語言中對應的概念是 closure。當ㄧ段能被執行的程式碼是需要兩個要素:
在Ruby的設計中,Block 就具備以上兩個特質,是一個隨時準備好可以執行的程式碼區塊。
看著上圖中 Bindings 的概念,再想想以下的範例,
def my_method
x = "Goodbye"
yield("Ruby")
end
x = "Hello"
my_method { |y| "#{x}, #{y} world" } # => "Hello, Ruby world"
當我們建立 block{ |y| "#{x}, #{y} world"} 的時候也同時抓住了連接的 local bindings, 例如:x ( variable: "Hello")。
接著把建立好的 block 傳進去方法裡,但是 my_method() 方法也有自己的 bindings 也是 x (variable: "Goodbye"),這個時候 block 看到自己的 bindings 已經就有 x 值了,而無視方法內的 x 值。
再來看看下面程式碼:
def just_yield
# 方法內無法取得 top_level_variable 變數
yield
end
top_level_variable = 1
# 在建立 block 的同時就會把 top_level_variable 抓進去local bindings裡面
just_yield do
top_level_variable += 1
local_to_block = 1 # 在local bindings的區域變數
end
top_level_variable # => 2
local_to_block # => "undefined local variable or..."
Scope (作用域,或稱為有效範圍) 就是指在該程式碼範圍內可以使用的變數、方法。
維基百科所定義的 scope:
在電腦程式設計中,作用域(scope,或譯作有效範圍)是名字(name)與實體(entity)的繫結(binding)保持有效的那部分電腦程式。不同的程式語言可能有不同的作用域和名字解析。而同一語言內也可能存在多種作用域,隨實體的類型變化而不同。作用域類別影響變數的繫結方式,根據語言使用靜態作用域還是動態作用域變數的取值可能會有不同的結果。
先來看一段的程式碼:
name = "Kevin"
def introduce
puts "Hi, my name is #{name}" # => "undefined local variable ..."
end
# 會有錯誤訊息的原因是:方法內的 scope 是無法看到外面的變數。
以下是書中的範例:
# 可使用 local_variables 取得當下區域變數的值
v1 = 1
self # main
class MyClass
v2 = 2
local_variables # => [:v2]
def my_method
v3 = 3
local_variables
end
local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method # => [:v3]
obj.my_method # => [:v3]
local_variables # => [:v1, :obj]˙
我們可從 self 得知當下的環境是 main,說明了 v1 是位於執行環境下的最頂層,也是在 top-level scope 裡的變數。
接著進入 MyClass 類別後,代表了進入了ㄧ個新的 scope (有效範圍),先前在 top-level scope 所定義的變數就不能再被使用。
在 MyClass 的有效範圍裡, 程式定義了 v2 以及 my_method() 方法。等到程式跑完 MyClass 類別後,才會又回到 top-level scope。
在此範例中,我們了解到不管程式中的 scope 如何改變,舊的 bindings 總是會隨著 scope 而被新的 bindings 取代。但 Global variable (全域變數) 則不在此限,因為 global variables 在任何一個 scope 都可以被使用。
這樣不被 scope 所限制的特質,卻也增加了日後除錯及維護的難度。