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