理解Ruby中的做用域

 

標題圖片

  做用域對於Ruby以及其它編程語言都是一個須要理解的相當重要的基礎知識。在我剛開始學習ruby的時候遇到不少諸如變量未定義、變量沒有正確賦值之類的問題,歸根結底是由於本身對於ruby做用域的瞭解不夠,但在你看看完個人這篇文章後,相信你不會再擔憂會遇到這些頭疼的問題。程序員

  什麼是做用域?編程

  當談論到做用域的時候,應該立刻想到變量可見性這兩個詞,變量可見性是做用域的主要內容,沒錯,做用域就是關於在代碼的什麼地方什麼變量是可見的,當你充分了解了做用域後,給你一段代碼,你能夠輕易知道此時什麼變量是可見的,還有最重要的是知道什麼變量在這段代碼執行時是不可見的。安全

  那就從一開始的地方就將全部變量定義好,讓全部變量在程序的全部地方都是可見的,不就能夠免除做用域的問題了?這樣不是讓生活更簡單嗎?嗯,但事實並非這樣......ruby

  你也許知道不少對立的程序員陣營,如:函數式編程陣營與面向對象編程陣營、不一樣的變量命名風格陣營、不一樣的代碼格式陣營等等,但從沒有人據說過支持去除做用域的陣營,特別是那些有着豐富編程經驗的程序員更是保留做用域的忠實支持者,爲何? 由於若是你編程的經歷越多,你會愈來愈發覺對全部變量在整個程序中保持可見是多麼愚蠢、破壞性的行爲,由於一開始就將全部變量定義並對整個程序均可見,那麼在程序運行時你很難追蹤何時、哪一段代碼對哪一個變量作了修改,而對於多人協做的工程,當面對成千上萬行的代碼時你很難知道某個變量是誰定義的?在什麼地方被賦值?大量使用全局變量會使得你的程序變得難以檢測追蹤、運行結果難以預測,若是使用全局變量,你會遇到一個很棘手的問題就是如何給成千個全局變量進行惟一命名。閉包

  做用域提供開發者一個實現相似計算機安全系統中的最少權限原則的方式,試想一下你正在開發一個銀行系統,而全部人均可以進行讀寫全部的數據,某我的對存款進行了更改但不能肯定他是這筆存款的全部者,這將是多麼可怕的一件事!編程語言

  Ruby變量做用域快速瀏覽函數式編程

  你可能已經對ruby的變量做用域有所瞭解,但我發現大部分教程都是對變量類型僅僅作一個簡單的介紹,而沒有對其有一個精確的講解,下面是對於ruby中各種型變量的一個詳細介紹:
函數

  類變量(以@@爲前綴):僅對定義該類變量的類及其子類可見。學習

  實例變量(以@爲前綴):對定義該 變量的類的實例及其實例方法可見,但不能夠直接被類使用。測試

  全局變量(以$爲前綴):對整個ruby腳本程序可見。

  局部變量:僅在局部代碼塊中可見,這也是在編程中最常用到和容易出現問題的變量類型,由於局部變量的做用範圍依賴不少的上下文代碼塊。

  下面用一張圖片簡潔明瞭地闡述4種變量做用域的關係。

  接下來的篇章我會專一於介紹這局部變量。從個人經驗以及與他人交談中發現大部分做用域的問題都是因爲對局部變量沒有一個很好的理解。

  局部變量何時被定義?

  在ruby語言中,對於在一個類中定義的實例變量(如@variable)不須要顯式提早聲明,在類的方法中嘗試獲取一個還未聲明的實例變量會返回nil,而當你嘗試獲取一個未聲明的局部變量的值時會拋出NameError錯誤 (undefined local variable or method)。

  Ruby解釋器在看到局部變量賦值語句時將該變量加入局部做用域,須要注意的是不管該局部變量的賦值是否會執行,只要ruby解釋器看到程序存在該變量賦值語句就會將該變量加入局部做用域,因此像下面的代碼是能夠正常執行而不報錯的。

 

if false # the code below will not run
  a = 'hello' # ruby解釋器看到該條語句將a變量加入局部做用域
end
p a # nil, 由於對a的賦值語句沒有執行

 

  你能夠嘗試下刪除 a = ‘hello’ 這條語句,看看會有什麼狀況發生。

  局部變量命名衝突

  假設你有如下代碼

def something
  'hello'
end

p something
==> hello
something= 'Ruby'
p something
==> Ruby #'hello' is not printed

  在ruby中方法的調用能夠像變量同樣不需顯式添加一個括號和方法接收對象,因此你可能會遇到像上面代碼的命名衝突問題。

  當你的ruby代碼中存在同名的變量名和方法名,同名的變量會以較高的優先級覆蓋掉同名的方法,但這並不表示你不能再調用該方法,此時能夠經過在方法調用時顯式添加括號或者在調用方法前顯式添加self做爲方法接收對象。

def some_var; 'I am a method'; end
some_var = 'I am a variable'
p some_var # I am a variable
p some_var() # I am a method
p self.some_var # I am a method. 顯式使用self對象調用some_var方法

  一個頗有效的判斷變量是否在做用域以外的方法

  首先在你的代碼段中找到你要查看的變量 ,接着一直往上查找改變量,直到你找到該變量,這時會有兩種狀況:

  1. 到了做用域的起始地點(def/class/module/do-end 代碼塊的開頭)
  2. 找到對該變量賦值的語句

  若是你在遇到2以前先遇到1的狀況,那麼頗有可能你的代碼會拋出NameError錯誤,若是你在遇到1以前先遇到狀況2,那麼恭喜你,該局部變量就在這段代碼的做用域當中。

  實例變量 vs 局部變量

  實例變量屬於某個對象,在該對象的全部方法中均可用,當局部變量是屬於某個特定的做用域,僅在該做用域下可用。實例變量在每一個新實例中可用進行修改,而局部變量會在進入一個新做用域時被改變或者覆蓋,那如何知道做用域何時會改變?答案是:做用域門。

  做用域門:理解做用域相當重要的一個概念

  當使用下面這些語句時,你猜測會對做用域產生什麼影響?

  1. 使用class關鍵字定義一個類;
  2. 使用module 定義一個模塊;
  3. 使用def關鍵字定義一個方法。

  當你使用這些關鍵字的時候你就開闢了一個新的做用域,至關於ruby打開了一扇門讓你的代碼進入一個全新的上下文環境。全部的class/def/module 定義被成爲做用域門,由於它們開啓了一個新的做用域,在這個做用域中全部的舊做用域都再也不可用,舊的局部變量會被新的局部變量所替代。

  若是你對上面的陳述感到疑惑,不要緊,經過下面的例子可讓你更好地掌握這一律念。

v0 = 0
class SomeClass # 開啓新做用域
  v1 = 1
  p local_variables # 打印出全部局部變量

  def some_method # 開啓新做用域
    v2 = 2
    p local_variables
  end # 做用域關閉
end # 做用域關閉

some_class = SomeClass.new
some_class.some_method

  當你運行上面的代碼後,你會看到分別打印出[:v1]、[:v2],爲何v0不會在SomeClass中的局部變量中?v1再也不some_method方法中?正是由於class和def關鍵字分別開闢了新的做用域,將舊的做用域替代了,舊做用域的局部變量在新的做用域中便不復存在了,爲何說做用域是被"替代」了?由於舊的做用域只是暫時被替換而已,在新做用域關閉時會再次回到舊的做用域,在some_class.som_method這條語句以後運行p local_variables你會看到v0從新出如今局部變量列表中。

  打破做用域門

  正如你所見,經過class/def/module會限制局部變量的做用域,而且會屏蔽掉以前的做用域,使得原來定義的變量在新做用域中不可見,那假如咱們想要處理做用定義的方法、類、模塊以外的局部變量,應該如何打破做用域之間的隔離?

  答案很簡單,只須要用方法調用的方式替換做用域門的方式,就是說:

  • 用Class.new 代替 class
  • 用Module.new 代替 module
  • 用define_method代替def

  下面是一個例子:

v0 = 0
SomeClass = Class.new do
  v1 = 1
  p local_variables

  define_method(:some_method) do
    v2 = 2
    p local_variables
  end
end

some_class = SomeClass.new
some_class.some_method

  運行上面的代碼後,會打印出[:v1, :v0, :some_class]和[:v2, :v1, :v0, :some_class]這兩行局部變量名,能夠看到咱們成功地打破了做用域的限制,這歸功於咱們接下來須要學習的ruby中的blocks的功能。

  Blocks也是做用域門的一種嗎?

  你也許會認爲blocks也是做用域門的一種,畢竟它也建立了一個包含局部變量的做用域,而且在blocks中定義的局部變量在外部是不可訪問的,就像下面的例子同樣:

sample_list = [1,2,3]
hi = '123'
sample_list.each do |item| # block代碼塊的開始
  puts hi # 是打印出123仍是拋出錯誤?
  hello = 'hello' # 聲明並賦值給變量hello
end

p hello # 打印出‘hello’仍是拋出undefined local variable 異常

  如你所見,在blocks代碼塊中定義的變量‘hello’是隻存在block做用域中的局部變量,外部不能訪問也不可見。

  若是block代碼塊是一個做用域門,那麼在puts hi這條語句執行時應該會觸發異常,但在block中卻能成功打印出hi的值,並且在blocks中你不只能訪問hi的值,而且可以對其進行修改,嘗試在do/end代碼塊中修改hi的值爲‘456’,你會發現外部變量 hi 的值成功被修改。

  那若是不想讓block中的代碼修改外部局部變量的值呢?這是咱們可使用block-local variable(相似方法中的形參),只需在block中將參數用 ; 分割後填寫外部同名的局部變量名(這些變量在block中會成功block-local variables),在block裏面對這些變量的修改不會影響其在外部原來的值,下面是一個例子:

hi = 'hi'
hello ='hello'
3.times do |i; hi, hello|
  p i
  hi = 'hi again'
  hello = 'hello again'
end
p hi # "hi"
p hello # "hello"

  若是你在block的參數列表中移除 ; hi, hello ,在代碼塊外面你會發現變量 hi 和 hello 的值變成‘hi again' 和 'hello again'了。

  記住使用do和end建立block代碼塊的同時會建立一個新的做用域。

[1,2,3].select do |item| # do is here, new scope is being introduced
  # some code
end

使用each、map、detect或者其它方法,當你使用do/end建立代碼塊當作參數傳給這些方法,只不過是建立了一個新的做用域。

Blocks和做用域的一些小怪癖

試想下面的代碼會輸出什麼:

y to guess what will this Ruby code print:

2.times do
  i ||= 1
  print "#{i} "
  i += 1
  print "#{i} "
end

  你是否是覺得會輸出 1 2 2 2 ?但答案是1 2 1 2 ,由於每一次的迭代會建立一個新的做用域並重置局部變量。所以,在這段代碼中咱們分別建立了兩個迭代,每次迭代開始都將變量重置爲1。

  那麼你認爲下面的代碼會輸出什麼?

def foo
  x = 1
  lambda { x }
end

x = 2

p foo.call

  答案是1,由於blocks和blocks對象看到的是定義在其內的做用域而不是調用該block時的做用域。這與它們在ruby中是被看做閉包有關,閉包是一種對代碼的包含,從而使該段代碼帶有如下特徵:

  • 該部分代碼能夠像對象同樣被調用(能夠在定義以後經過call調用)
  • 當閉包被定義時,記錄該做用域下的變量

  這些特性給咱們在編寫無限數字生成器等狀況時提供方便:

def increase_by(i)
  start = 0
  lambda { start += i }
end

increase = increase_by(3)
start = 453534534 # won't affect anything
p increase.call # 3
p increase.call # 6

  你能夠利用lambda表達式方便地對變量進行延遲賦值:

i = 0
a_lambda = lambda do
  i = 3
end

p i # 0
a_lambda.call
p i # 3

  你認爲下面代碼段中的最後一行代碼會輸出什麼?

a = 1
ld = lambda { a }
a = 2
p ld.call

  若是你的回答是1,那麼你就錯了,最後一行語句會輸出2。咦,等一下,lambda表達式沒有看到它們的做用域嗎?若是你再仔細想想,你會發現這實際上是正確的,a = 2也在lambda表達式的做用域當中,真如你所見到的,lambda表達式在其被調用時纔開始計算變量的值。若是你沒有考慮到這點,將會容易觸發難以追蹤的bug。

  如何在兩個方法中共享變量

  一旦咱們知道如何打破做用域門,咱們就能夠利用這些來實現很驚人的效果,我從ruby元編程這本書中學習到這些知識,這本書幫助我理解ruby背後的做用域的工做原理。下面是一個例子:

def let_us_define_methods
  shared_variable = 0

  Kernel.send(:define_method, :increase_var) do
    shared_variable += 1
  end

  Kernel.send(:define_method, :decrease_var) do
    shared_variable -= 1
  end
end

let_us_define_methods # 運行這條語句後在初始化increase_var、decrease_var方法的定義
p increase_var # 1
p increase_var # 2
p decrease_var # 1

  是否是很是簡潔?

  頂層做用域

  什麼事頂層做用域?如何判斷當前代碼是否在頂層做用域中?處在頂層做用域意味着你尚未調用任何方法或者你全部的方法都已經調用結束並返回。

  在ruby中,萬事皆對象。即便你處在頂層做用域中,你一樣也是處在一個對象中(該對象繼承自Object類,稱之爲main對象),本身嘗試運行下面的代碼進行檢驗。

p self # main
p self.class # Object

  我在哪裏?

  在調試中,若是你知道當前self的值會解決你不少使人頭疼的問題,當前的self值影響到實例變量和沒有顯式指明接收對象的方法調用,若是你已經肯定你的變量或者方法已經定義(你在程序中看到該變量/方法的定義)但仍是觸發undefined method/instance variable錯誤,那麼頗有可能的self值是有問題的。

  小測試: 哪些變量是可用的?

  經過下面的小測試來確認你是否已經掌握了本文提到的知識,設想你是一個小型ruby調試器來運行下面的代碼:

class SomeClass
  b = 'hello'
  @@m = 'hi'
  def initialize
    @some_var = 1
    c = 'hi'
  end

  def some_method
    sleep 1000
    a = 'hello'
  end
end

some_object = SomeClass.new
some_object.some_method

  以sleep 1000這條語句做爲斷點,你能夠看見什麼?哪些變量在如今這個斷點是可用的?在你運行代碼前先想一下並給出本身的答案,你的答案不該該僅僅包含可用的 變量,還應該說明爲何該變量在此時是可用的。

  正如我以前提到的,局部變量是綁定在做用域中的,some_method函數定義時會闖將一個新的做用域並將其以前的做用域替換成新的做用域,在新的做用域當中你,a變量是惟一一個可用的變量。

  實例變量是與self綁定的,在上面的代碼中,some_object是當前的對象,@some_var是對於整個some_object可用的實力變量。類變量也相似,@mm實例變量在當前對象中也是可用的。局部變量b和c由於做用域門的緣由在方法中變得不可見,若是你想讓它們變得可見,請見打破做用域門的小節。

 但願個人文章對你有幫助,若是有疑惑,請在評論進行評論。

 

----------------------------------------分割線----------------------------------------------------

  本文翻譯自Darko GjorgjievskiUnderstanding Scope in Ruby,以爲這篇文章對於做用域的講解比較好,對於ruby做用域的理解頗有幫助,因此就翻譯下來。

相關文章
相關標籤/搜索