在許多編程語言中都會有閉包這個概念。今天主要來談談Ruby中的閉包,它在這門語言中地位如何,以什麼形式存在,主要用途有哪些?node
維基百科裏對閉包地解釋以下git
In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
複製代碼
看起來很複雜是吧?其實我也看不太懂,建議英文很差的人仍是學我去看中文版。通俗來說,閉包就是一個函數,它能夠跟外部做用域打交道,訪問並修改外部做用域的變量。在咱們所熟悉的JavaScript這門語言中,全部的函數/方法都是閉包。github
let a = 1;
function add_a() {
return a += 1;
}
console.log(a)
console.log(add_a())
console.log(add_a())
console.log(add_a())
console.log(a)
複製代碼
結果以下編程
1
2
3
4
4
複製代碼
可見,函數add_a
能夠自由訪問外部做用域的變量a
,而且可以在函數調用過程當中持續維護着變量的值。這種特性爲函數的柯里化帶來了可能。c#
function add(a, b) {
return a + b
}
console.log(add(1,2)) // => 3
複製代碼
柯里化以後可得api
function add(a) {
return function(b) {
return a + b
}
}
console.log(add(1)(2)) // => 3
複製代碼
得益於閉包的特性,上述兩個函數雖然調用方式不一樣,不過它們所完成的工做是等價的,咱們能夠藉此寫出許多有趣的代碼。然而閉包若是使用不當,或許會不當心修改了不該該修改的外部變量,特別是這些外部變量被多個程序單元共享的時候,可能會引起意想不到的系統問題。Ruby在設計的時候也考慮到了這種問題,因而只要是經過def
定義的方法,它都會建立一個封閉的詞法做用域,在該做用域內不可訪問外部的局部變量,外部信息只可以經過參數的形式傳入到函數中。好比,下面這個代碼片斷是會報錯的ruby
a = 100
def add_a
a = a + 1
end
puts add_a
複製代碼
Traceback (most recent call last):
1: from a.rb:7:in `<main>' a.rb:4:in `add_a': undefined local variable or method `a' for main:Object (NameError) 複製代碼
然而,若是沒有閉包,Ruby這門語言所可以提供的靈活性就頗有限了。Matz也考慮到了這點,Ruby中並非沒有閉包,它只是以另外一種方式來展示--代碼塊。代碼塊也是Ruby元編程的重點內容,接下來咱們以代碼塊的形式來從新定義add_a
方法。bash
a = 100
define_method :add_a do
a = a + 1
end
puts add_a # => 101
puts add_a # => 102
puts add_a # => 103
複製代碼
該例子中採用了define_method
搭配代碼塊來定義方法,使得咱們能夠在函數體的中訪問外部做用域的局部變量a
,使得該函數可以達到咱們預期的效果。一個方法的定義,是否要造成封閉的做用域,不一樣的語言可能會有不一樣的權衡,Ruby特意採用了代碼塊來表示閉包,有別於通常的方法定義,爲這門語言增添了很多色彩。閉包
PS: 固然形如@xxxx
的實例變量便不受這種封閉做用域的制約。由於實例變量自己就是實例上下文共享的。app
在編程世界中,咱們簡單地稱可以做爲某個函數的參數,而且可以在該函數內部被調用的函數爲回調函數。許多人聽到回調函數就會想到回調地獄,然而我的以爲只要設計得當,並非全部回調都會淪爲地獄。在Ruby中幾乎每個經過def
關鍵字定義地方法都默認接收一個代碼塊來做爲回調函數,一般這個默認的回調函數參數並不須要顯式聲明,考慮如下代碼片斷
def print_message(message)
yield(message) if block_given?
puts 'The End!!'
end
print_message('Hello World') do |message|
puts "I will print the message #{message}"
end
複製代碼
結果以下
I will print the message Hello World
The End!!
複製代碼
好玩吧,咱們能夠在調用方法的時候,在末尾以代碼塊的形式來定義回調邏輯。在被調用的方法的內部,經過block_given?
來判斷是否有代碼塊傳入,若是有須要則經過關鍵字yield
來運行對應的代碼塊,並傳入相關的參數。這種以代碼塊做爲回調的方式,爲編碼帶來了必定的靈活性。然而或許在一些場景中這種隱式接收代碼塊的方式並非那麼直觀,咱們也能夠顯式地去聲明這個參數。
def print_message(message, &block)
block.call(message) if block_given?
puts "The End!!"
end
print_message('Hello World') do |message|
puts "I will print the message #{message}"
end
複製代碼
只是在這種場景中對應的參數&block
會把咱們傳入的代碼塊轉換成Proc
對象,因而在這個例子中須要經過Proc#call
方法來運行對應的代碼塊,而再也不用yield
關鍵字了。固然咱們也能夠直接往被調用的方法中傳入一個Proc
的對象
callback = Proc.new do |message|
puts "I will print the message #{message}"
end
def print_message(message, &block)
block.call(message) if block_given?
puts "The End!!"
end
print_message('Hello World', &callback)
複製代碼
打印結果都是同樣的
I will print the message Hello World
The End!!
複製代碼
以上兩種代碼塊充當回調的方式,是Ruby中編碼的經常使用手段。回調邏輯始終做爲「最後一個參數」傳入到被調用的方法中去,這或許也是一種約定優於配置的表現吧。
剛開始接觸Ruby的時候,我總以爲代碼塊是一個反人類的設計,明明就是一個閉包,爲什麼要設計得這麼異類。更奇怪的是,許多業內人士都以爲代碼塊是Ruby最偉大的發明之一。後來接觸多了漸漸也就習慣了,代碼塊的優雅配合上其閉包的特性,再加上上面所說的一些回調的相關約定,爲Ruby這門語言增色很多。代碼塊有兩種表達方式{ .... }
和do ... end
。通常對於單行的代碼塊會採用第一種形式,對於多行的代碼塊會採用第二種形式。在Ruby的開源世界中,代碼塊幾乎無處不在,下面咱們來看一些常見的案例
Ruby承襲於Lisp,代碼塊的運行會自動返回最後一條語句或者表達式的值,因而有些庫也考慮到了用代碼塊來進行容錯處理。就拿Hash
的實例來做作個例子,咱們但願當Hash
實例對應的鍵值對不存在的時候給它一個默認值,常見的作法是
> hash = {}
> value = hash['a'] ? 'default value' : hash['a']
=> "default value"
複製代碼
熟悉JavaScript的人應該對上面這種代碼不陌生,真所謂是囉嗦至極。爲了使代碼更加優雅咱們能夠採用Hash#fetch
接口來取值,當Hash#fetch
接口找不到對應的鍵值對的時候就會觸發異常
> hash = {}
> hash.fetch('a')
Traceback (most recent call last):
3: from /Users/lan/.rvm/rubies/ruby-2.5.3/bin/irb:11:in `<main>' 2: from (irb):12 1: from (irb):12:in `fetch' KeyError (key not found: "a") 複製代碼
這個時候咱們能夠採用代碼塊來作容錯,當找不到對應鍵的時候爲取值操做提供一個默認值
> value = hash.fetch('a') { 'default value' }
=> "default value"
複製代碼
相對於第一種方式第二種方式更加優雅,也更有Ruby味一些。雖然說計算機世界是由0,1組成的,非此即彼。可是Ruby社區並不崇尚Python社區的絕對正確,每一個人的偏向不一樣,咱們能夠選擇本身喜歡的方式去完成工做。
另一個代碼塊用得比較普遍的地方應該就是DSL了,許多優秀的Ruby開源項目都會有相應的DSL。下面是Ruby模板渲染庫RABL的配置代碼
Rabl.configure do |config|
# Enabling cache_all_output will cause an orders cache entry to be used in all templates
# matching orders.cache_key, which results in unexpected behavior on Spree api response.
# For more about this option, see https://github.com/nesquena/rabl/issues/281#issuecomment-6780104
config.cache_all_output = false
config.cache_sources = !Rails.env.development?
config.view_paths = [Rails.root.join('app/views/')]
end
複製代碼
這是一個簡單的DSL,經過暴露模塊內部的config
實例,而後在調用者的上下文中去配置實例相關的屬性。這裏的代碼塊其實也充當了回調函數的角色,它讓咱們的配置邏輯能夠被統一規劃到一個區間當中,不然的話可能你得寫出相似這樣的配置代碼
config = Rabl::ConfigureXXXX.new
config.cache_all_output = false
config.cache_sources = !Rails.env.development?
config.view_paths = [Rails.root.join('app/views/')]
複製代碼
不管怎麼看都是DSL的方式比較優雅對吧?相似的DSL還有不少,這裏不一一舉例了,這些DSL如何去實現也不在本篇文章的討論範圍內。
代碼塊能夠被當作是一個「蹩腳」的函數,雖然說通常狀況下它能夠做爲某個方法的回調,可是它不像JavaScript中的函數那樣能夠獨立存在,它必需要依賴其餘的機制。當咱們要用代碼塊去定義一個匿名函數時,須要搭配lambda
關鍵字或者Proc
類來實現
> c = lambda() {}
=> #<Proc:0x00007ff47b8546a8@(irb):6 (lambda)>
> c.class
=> Proc
> (lambda() { 'hello' }).call
=> "hello"
> (lambda() { 'hello' })[]
=> "hello"
> (Proc.new { 'hello' }).call
=> "hello"
> (Proc.new { 'hello' })[]
=> "hello"
複製代碼
以上都是經常使用的定義匿名函數的方式,本質上它們都是Proc
類的實例,須要顯式地利用Proc#call
方法或者語法糖[]
來調用它們。
這篇文章簡單地介紹了一下閉包的概念,閉包跟通常封閉做用域的方法有何不一樣之處。區別於通常的方法,閉包在Ruby中以代碼塊的形式出現,它在Ruby世界中幾乎無處不在,充當了一等公民。這種區分,不只使咱們的Ruby代碼更加優雅,增添了可讀性,還使得咱們的編碼過程更加簡單。