สวัสดีปีใหม่ และยินดีต้อนรับกลับมาสู่ Ruby Magic! ใน winter episode นี้ 我們將ดำดิ่งสู่ bindings 和 scopes 所以穿上滑雪板跟著我們深入森林吧
上次 我們通過比較 blocks, procs 和 lambdas 看了 Ruby 中的 closures 除了三種類型之間的差異 我們觸及了什麼定義了一個 closure
一個 closure 是一個帶有 environment 的 first-class function Environment 是對 closure 被創建時存在的 variables 的 mapping Closure 保持對這些 variables 的訪問 即使它們被定義在另一個 scope 中
我們探索了 Ruby 對 first-class functions 的等價物 但我們方便地跳過了 environments 在這一集 我們將看那個 environment 如何為 closures, classes 和 class instances 工作 通過檢查 Ruby 如何通過其 bindings 處理 lexical scope
Lexical Scope
在 programming 中 scope 指的是在代碼特定部分可用的 bindings 一個 binding 或 name binding 將名稱綁定到內存引用 像 variable 的名稱到其值 Scope 定義 self 意味著什麼 可以調用哪些 methods 和有哪些 variables 可用
Ruby 像大多數現代編程語言 使用 static scope 通常稱為 lexical scope (相對於 dynamic scope) 當前 scope 基於代碼結構並決定特定代碼部分可用的 variables 這意味著當代碼在 methods, blocks 和 classes 之間跳轉時 scope 會改變 — 因為它們都可以有不同的 local variables
def bar
foo = 1
foo
end
bar # => 1
在這個方法中 我們在方法內部創建一個 local variable 並將其打印到控制台 該變量在方法內部 in scope 因為它在那裡創建
foo = 1
def bar
foo
end
bar # => NameError (undefined local variable or method `foo' for main:Object)
在這個例子中 我們在方法外部創建變量 當我們在方法內部調用變量 我們得到錯誤 因為變量 out of scope Local variables 是 tightly scoped 意味著一個方法不能訪問在其外部定義的變量 除非它被作為參數傳遞
@foo = 1
def bar
@foo
end
bar # => 1
雖然 local variables 在局部可用 instance variables 可用於類實例的所有方法
Inherited Scopes and Proc Bindings
正如我們在前面的例子中看到的 scope 基於代碼中的位置 在方法外部定義的 local variable 不在方法內部的 scope 中 但可以通過將其變為 instance variable 來使其可用 方法不能訪問在其外部定義的 local variables 因為方法有自己的 scope 和自己的 bindings
Procs (包括 blocks 和 lambdas, 通過延伸) 是不同的 每當一個 proc 被實例化 一個 binding 被創建 它繼承在塊被創建的上下文中 local variables 的引用
foo = 1
Proc.new { foo }.call # => 1
在這個例子中 我們設置一個名為 foo 的變量為 1 內部 在第二行創建的 Proc 對象創建一個新 binding 當調用 proc 時 我們可以詢問變量的值
由於 binding 在 proc 被初始化時創建 我們不能在定義變量之前創建 proc 即使塊直到變量被定義後才被調用
proc = Proc.new { foo }
foo = 1
proc.call # => NameError (undefined local variable or method `foo' for main:Object)
調用 proc 將產生 NameError 因為變量未在 proc 的 bindings 中定義 因此 proc 中訪問的任何變量應在 proc 被創建前定義或作為參數傳遞
foo = 1
proc = Proc.new { foo }
foo = 2
proc.call # => 2
然而 我們可以在主上下文中定義後更改變量 因為 proc 的 binding 持有對它的引用而不是複製它
foo = 1
Proc.new { foo = 2 }.call
foo #=> 2
在這個例子中 我們可以看到 foo 變量在 proc 內部和外部指向同一個對象 我們可以在 proc 內部更新它以使外部變量也被更新
Bindings
為了跟踪當前 scope Ruby 使用 bindings 它們在代碼中每個位置封裝執行上下文 binding 方法返回一個描述當前位置 bindings 的 Binding 對象
foo = 1
binding.local_variables # => [:foo]
Binding 對象有一個名為 #local_variables 的方法 返回當前 scope 中所有可用 local variables 的名稱
foo = 1
binding.eval("foo") # => 1
代碼可以通過使用 #eval 方法在 binding 上評估 上面的例子不是很有用 因為簡單調用 foo 會有相同結果 然而 由於 binding 是一個可以傳遞的對象 它可以用於一些更有趣的事情 讓我們看個例子
A Real-Life Example
現在我們在車庫的安全環境中學習了 bindings 像把它們帶到雪坡上玩雪一樣 除了 Ruby 內部在整個語言中使用 bindings 有一些情況 binding objects 被 explicit 使用 一個好例子是 ERB — Ruby 的 templating system
require 'erb'
x = 1
def y
2
end
template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"
在這個例子中 我們創建一個名為 x 的變量 一個叫 y 的方法 和一個引用兩者的 ERB template 然後我們將當前 binding 傳遞給 ERB#result 它評估模板中的 ERB tags 並返回一個填充變量的字符串
在底層 ERB 使用 Binding#eval 在傳遞的 binding 的 scope 中評估每個 ERB tag 的內容 一個對上面例子有效的 simplified implementation 可能長這樣:
class DiyErb
def initialize(template)
@template = template
end
def result(binding)
@template.gsub(/<%=(.+?)%>/) do
binding.eval($1)
end
end
end
x = 1
def y
2
end
template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"
DiyErb 類在初始化時接收 template string 其 #result 方法找到所有 ERB tags 並用評估其內容的結果替換它們 為此 它在傳遞的 binding 上調用 Binding#eval 並帶上 ERB tags 的內容
通過在調用 #result 方法時傳遞當前 binding eval 調用可以訪問在方法外部定義的變量 甚至在類外部 而不必顯式傳遞它們
Did we lose you in the woods?
我們希望你享受我們的滑雪之旅深入森林 我們在 glossing over 之後更深入 scopes 和 closures 我們希望沒有讓你在森林迷路 請讓我們知道如果你想了解更多關於 bindings 的內容 或有任何其他 Ruby 主題你想深入研究
謝謝跟隨我們 請在離開前把雪從你的 bindings 上踢掉 留給下一位開發者 如果你喜歡這些神奇之旅 你可能喜歡訂閱 Ruby Magic 以在我們發布新文章時收到電子郵件 大約一個月一次