Ruby 的作用域 Scope
作用域存在于任何編程語言中,如果不夠了解作用域,經常會出現變量未定義、錯誤分配變量值等等問題,本章節中會對 Ruby 的作用域做深度剖析。
1. 作用域是什么
作用域就是變量的有效使用范圍。當提到作用域的時候,您應該了解,在Ruby中任何一行在該上下文中,哪些變量可用,哪些變量不可用。
有人會說,那么讓變量在全局的任何地方都可用不就好了,如使用全局變量(Global Variable),這樣就不會因為作用域而煩惱。
但是當您從事了編程工作一段時間后,您會發現,全局變量非常地不可控,任何人都有能力修改這個變量,這個變量究竟是被誰讀了、被誰寫了,問題一單產生很難追蹤。并且全局變量要求我們的命名必須不同,這樣的話在一個項目中,可能會出現上千個變量名。
在之前Ruby的變量章節中我們講解了4種變量的類型?,F在按照作用域大小來劃分他們:
-
全局變量(Global Variable)的作用域包括頂級作用域、每一個類、實例、局部(任意地方);
-
類變量(Class Variable)的作用域包括類、實例、局部;
-
實例變量(Instance Variable)作用域包括實例、局部;
-
局部變量(Local Variable)作用域僅有局部。
2. 頂級作用域
頂級作用域(Top Level Scope)意味著您未調用任何方法,或者所有的方法都已經返回。簡單來講,當我們剛打開irb
或在一個沒有任何類或方法的Ruby腳本之中,我們所處的就是頂級作用域。
在Ruby中一切皆為對象,即使您處于頂級作用域,也位于一個對象之中,它的名字叫做main
,屬于類Object
。
下面是irb
的示例:
$irb
> puts self
main
=> nil
> puts self.class
Object
=> nil
或在一個空腳本中輸入:
p self
p self.class
# ---- 輸出結果 ----
main
Object
3. 作用域門
當我們執行下面三種操作的時候會打開作用域門(Scope Gate),進入一個全新的作用域(完全不同的上下文):
- 定義一個類(
class SomeClass
); - 定義一個模塊(
module SomeModule
); - 定義一個方法(
def some_method
)。
我們用一個局部變量的例子來解釋作用域門的概念。
局部變量有這樣的特性,當我們輸出一個一個未定義的局部變量會拋出 NameError 的異常。
實例:
puts a
# ---- 輸出結果 ----
undefined local variable or method `a' for main:Object (NameError)
Tips:實例變量和全局變量擁有默認值,為
nil
。
當我們在作用域范圍內為局部變量定義,不管代碼是否執行,Ruby 的解釋器都會將這個局部變量放入作用域。
實例:
if false
a = 1 # 代碼不執行,但是Ruby的解釋器將局部變量a放入了當前作用域
end
p a # nil 代碼未執行,因此未初始化
# ---- 輸出結果 ----
nil
我們可以通過local_variables
這個方法來獲取當前作用域中所有的局部變量。
實例:
v0 = 0
def a_method
v1 = 1
p local_variables
end
a_method
p local_variables
# ---- 輸出結果 ----
[:v1]
[:v0]
解釋:當我們定義 v0 的時候,v0 在頂級作用域中,然后我們定義了a_method
開啟了一個新的作用域,之后在a_method
里面定義了 v1 變量,因為作用域門的限制,v0 并不會在a_method
的作用域里面,因此局部變量列表只打印了 v1 變量。在調用完a_method
方法之后,我們輸出了頂級作用域的局部變量列表,顯示了而當前作用域只有 v0 在作用域內部,所以只打印了變量 v0。
和def
一樣,module
和class
也會打開作用域門,創建一個新的作用域,外部局部變量無法進行訪問。
4. 跨越作用域門
當使用module
、class
、def
來定義模塊、類、方法的時候會產生作用域門,大大限制了局部變量的使用范圍,那么我們有沒有一種方式來跨越作用域門呢?
答案是有的,我們需要改變一下定義模塊、類、方法的方式,不使用關鍵字,而使用方法去定義它們:
- 定義類:
Class.new
; - 定義模塊:
Module.new
; - 定義方法:
define_method
。
讓我們改寫一下上面的例子:
實例:
v0 = 0
define_method :a_method do
v1 = 1
p local_variables
end
a_method
p local_variables
# ---- 輸出結果 ----
[:v1, :v0]
[:v0]
解釋:從輸出結果我們可以看到,v0 成功跨越了作用域門進入到了a_method
里面。同時,變量 v1仍然只在方法的作用域里面。
5. 閉包
什么是閉包(Closure),簡言之在塊的作用域外面定義的變量可以在塊整個生命周期進行訪問,Ruby 有三種形式的閉包:Block、Proc、Lambda。Block可以將代碼塊傳給方法,Proc 和 Lambda 可以把代碼塊存儲在變量之中。(關于 Block 請看 Ruby 的塊 章節,Proc 和 Lambda 請看 Proc 和Lambda 章節)。
因此閉包并不是作用域門。
從跨越作用域門的例子中可以看到,定義類、模塊、方法的三種形式均使用到了塊,它允許引用作用域外的變量并開啟了新的作用域。
實例:
num = 1
(1..3).each do |i|
num += i
end
p num
# ---- 輸出結果 ----
7
解釋:由上面的例子我們可以看到在塊中我們拿到了變量num
,并且執行了操作。
那要是不希望塊訪問到外部變量要怎么辦呢,我們有下面這種形式。
實例:
hello = 'Hello'
hi = 'Hi'
1.times do |i; hi, hello|
p i
hello = 'Hello 2'
hi = 'Hi 2'
end
p hello
p hi
# ---- 輸出結果 ----
Hello
Hi
解釋:從輸出結果我們可以看到,我們并沒有修改了外部變量。我們在塊參數的末尾放置了一個分號(;
),然后追加我們不希望訪問到的外部變量名稱,就可以做到不去訪問外部變量。
如果我們去掉; hi, hello
的話,我們會得到Hello Hi
的結果。
6. 小結
本章中我們學習了作用域,使用class
、module
、def
會開啟作用域門 ,使用Class.new
、Module.new
、define_method
可以跨越作用域門。了解了閉包的概念,在閉包外定義的變量可以進入閉包內部使用,以及可以使用分號讓外部的變量不可以進入閉包。