Learning Closure:學習閉包

 

先讓我回憶一下第一次見到這個詞時的情景:那是在2006年的某天,當時一個朋友像打了雞血一樣興奮的給我推薦了一個5句話就可以創建一個網站的框架——Rails,看完演示後我也像打了雞血一樣興奮的瞟了一眼這個框架背後的語言——Ruby。從此,我知道了一個讓我感到陌生、高深甚至深不可測到可以裝B的詞,這就是閉包。

讓我慚愧的是,我真的是時常拿這個詞出去裝B,但是卻長時間不知道它的真正含義。甚至一度覺得周圍跟我提起這個詞的人和我是一樣zhuangbility的。

就這樣,我到了2010年。閉包已經不再是一個讓人雞血沸騰的詞語了,我覺得是時候冷靜的瞭解一下這個概念了,所以我記下了這篇筆記。

首先記錄一個比較靠譜的定義:

閉包(Closure),詞法閉包(Lexical Closure)的簡稱,是由函數和與其相關的引用環境組合而成的實體。

當然,網上還會有很多其它的說法和定義,我只是在這裏記錄了一條我覺得比較靠譜的。即使如此,當我第一次看到上面這句話的時候也還是一頭霧水。所以如果你看完這句話就明白什麼意思了,那麼下面就不需要浪費時間了;但是如果你看完後也和我一樣迷糊,不要着急,讓我們慢慢的來,下面的解釋會讓我們更加“迷糊”。

我堅信例子是解釋一切問題最好的方式。但是爲難我的是,我應該用什麼語言做例子來演示這個問題呢?現在支持閉包的語言有很多,python、Ruby、perl、Lua以及JavaScript等等,當然熟悉這些語言的人估計也不會在這裏浪費他們的時間,不熟悉閉包的人可能恰好也不熟悉這些語言。我覺得用一個不熟悉的語言來演示一個晦澀的概念,應該屬於越摸越黑的那種。算了,我們一會兒再說這個事情,讓我冷靜一會兒。

函數

我們先簡單的聊聊函數吧,因爲閉包這個概念貌似最早來自於函數式編程(FP,Functional Programming),當然也不能深聊,畢竟第一這個筆記的主旨不在這裏,其次聊多了我就露餡兒了。->_<-

說說我對函數的理解(MD,維基百科怎麼打不開了,抄的地兒都沒有了)。還記得數學課上的那個f(x)=x^{2} + 1,這個就是函數啊。

明白了吧!以上就是我對函數的理解。

請盯着那個等式看一會兒,腦子裏想想你最熟悉的編程語言。那個f是不是像我們的函數名,那個x就是函數的參數(輸入),而x^{2} + 1就是它的結果(輸出)。我們程序中每一個函數不都遵循這樣一個原則麼——有零個或多個輸入,有一個輸出。雖然我不能妄斷方法和函數的區別,但是我們所說的方法不也是這樣麼。

接下來就是我們對f(x)的使用了。我們可以這麼用:f(y)=2f(x)+y。看着這個函數思考一下,我們程序中的函數是否也可以這麼寫呢?(當然,我說的不是格式,哥,你懂的!)

最後,對於我們的f(x)來說,如果我們輸入的值是固定的,那麼我們總能得到一個相同的輸出。比如我們輸入2,那麼我們總能得到5。

好了,讓我們來看看函數比較抽象的概念吧:

在數學領域,函數是一種關係,這種關係使一個集合裏的每一個元素對應到另一個集合裏的唯一元素。函數是將唯一的輸出值賦予每一輸入的"法則"。這一"法則"可以用函數表達式、數學關係,或者一個將輸入值與輸出值對應列出的簡單表格來表示。函數最重要的性質是其決定性,即同一輸入總是對應同一輸出(注意, 反之未必成立)。從這種視角,可以將函數看做"機器"或者"黑盒",它將有效的輸入值變換爲唯一的輸出值。通常將輸入值稱做函數的參數,將輸出值稱做函數的值。

至於函數式編程,真的不是一句兩句可以說清楚的。有興趣的話可以google一下。總之我們的程序也可以像上面的數學函數那樣來編寫和使用。

閉包

思來想去明白一個事兒,那就是既然哪個語言都不熟悉,也就沒有必要挑來挑去了。我就隨便摘抄了哈!!!

先看一個例子(JavaScript):

  1. function makeFunc() {  
  2.   var name = "Closure";  
  3.   function displayName() {  
  4.     alert(name);  
  5.   }  
  6.   return displayName;  
  7. }  
  8.   
  9. var myFunc = makeFunc();  
  10. myFunc(); 

應該不難懂的。函數 makeFunc 定義了一個局部變量 name 和一個函數 displayName,並且 displayName 是一個內部函數,它在 makeFunc 函數中被定義,僅在函數內部可見。displayName 函數內部沒有定義任何局部變量,它僅僅使用了外部函數中的變量。makeFunc 返回了定義好的 displayName 函數。緊接着我們調用 makeFunc 並將其返回的函數賦值給了 myFunc 變量。最後我們調用 myFunc 來得到結果。

不管你是不是理解裏面的運行機制,但是答案我們都可以猜想的出—— name 變量的值Closure被顯示了出來。也許這個例子太簡單了,簡單到我們想當然的認爲結果就是顯示 name 變量的值。但是慢着,讓我們深究一下就有意思了。

注意一下那個局部變量 name,看是不是能讓你想起些什麼。既然我們稱 name 爲局部變量,也就是說它的作用範圍(可見性)應該在 makeFunc 函數內部,只有當我們執行 makeFunc 的時候它才存在,當 makeFunc 函數執行完畢之後它就應該不存在了,爲什麼我們還可以看到我們想要的結果呢?

這就是因爲我們的 myFunc 成爲了閉包(Closure)。現在再讓我們回想以下我們一開始提到的那個靠譜的概念,閉包綁定了兩個東西:函數和函數定義時的環境。這個環境包括所有對於函數定義時可見的局部變量。所以,當我們創建 myFunc 時,它將 displayName 函數和局部變量 name 包了起來,形成了我們說的閉包。

如果我們對上面這個例子還有說不清道不明的感覺的話,那我們就再來一個清楚的例子(Lua):

  1. function do10times(fn)   
  2.   for i = 0,9 do
  3.     fn(i)
  4.   end  
  5. end 

  6. sum = 0  
  7. function addsum(i)
  8.   sum = sum + 1
  9. end
  10. do10times(addsum)
  11. print(sum)

這裏我們看到,函數 addsum 被傳遞給函數 do10times,被並在 do10times 中被調用10次。不難看出 addsum 實際的執行點在 do10times 內部,它要訪問非局部變量 sum,而 do10times 並不在 sum 的作用域內。這看起來也是無法正常執行的。


再一次,閉包幫了我們。當我們傳遞 addsum 時,就已經創建了閉包,它將 sum 和 addsum 包了起來形成了一個閉合的作用域。當我們把這個閉合的作用域當作一個整體來調用的時候,先把其中的引用環境覆蓋到當前的引用環境上,然後執行具體代碼,並在調用結束後恢復原來的引用環境。這樣就保證了函數定義和執行時的引用環境是相同的,問題也就迎刃而解了。

 

引用一句話總結一下:對象是附有行爲的數據,而閉包是附有數據的行爲。

閉包的應用

我覺得我應該說明白什麼是閉包了,下面該寫一下這個被說的沸沸揚揚的東西到底有什麼用處了。

嗯。。。怎麼說呢,關於閉包的應用我第一個能夠想到的就是重構——沒錯,就是那個讓你怎麼做都覺得不對的重構。其實就我個人來說,平時最頭疼的是感覺手上掌握的工具不夠或者還不犀利。當然,閉包可以爲我們的工具箱增加一定的份量。我們可以用它來重構出更加緊湊和抽象的代碼。

第二個能想到的是循環遍歷。我覺得大家對這個應該都不會陌生。還是舉個簡單的例子來說吧。不知道大家平時在做項目的時候是否碰到過重複重複再重複寫循環語句的時候呢?這個時候用Java來做的話,就不得不四處編寫for、while語句或者使用iterator。但是如果我們用支持閉包的語言的話,比如Ruby語言,就會方便很多,看看下面的代碼:

  1. nums = [10,3,22,34,17]  
  2. sum = 0  
  3. nums.each{|n| sum += n}
  4. print sum

另外再來一個更直觀的例子:遍歷數據庫查詢結果。用過Java的童鞋好好想想Java是如何從數據庫中取出ResultSet並且如何遍歷的吧。如果談及這個,估計Ruby的童鞋該笑了。不是沒有原因的,看看下面的代碼就知道了:

  1. require 'mysql'
  2. db=Mysql.new("localhost", "root", "password")
  3. db.select_db("database")

  4. result = db.query "select * from words"
  5. result.each {|row| do_something_with_row}
  6. db.close

第三個能夠想到的是關於資源和事務的管理。比如當我們想要讀取文件並對文件內容進行一些處理,這個時候打開文件、關閉文件以及過程中異常的處理其實都是可抽象的。也就是說不管我們對文件做什麼處理,打開、關閉和異常處理都是相同的。如果你是文件類庫的作者的話,就應該將對資源的管理抽象出來,讓用戶更多的關注文件的處理。看看Ruby的代碼就知道了:

  1. File.open(name) {|file| process_file(f)}

看了上面的代碼,你還會爲處理文件而感到頭疼麼?反正我是不會了。

關於閉包的作用還有很多很多,大家可以去慢慢的體會。總之,當我們掌握了閉包這個工具之後,它會給我們日常的coding工作帶來很多的好處。

寫在最後

時間可以沉澱很多的東西,尤其是在IT行業中。很多的概念或者技術都曾被炒的沸沸揚揚,但許多到最後都銷聲匿跡了。不過我們不能完全忘記它們,因爲沒有哪一項技術天生就是壞的,它的演化和發展肯定會對未來產生影響。閉包很早就存在了,直到近幾年才廣泛被大家接受。甚至Java 7也因爲閉包的加入而拖延了它的發佈日期。所以當一項技術逐漸成熟時,我們不能墨守成規的對其視而不見,應該及時的瞭解它們並擁抱它們。

Java語言現在還不支持閉包,Java 7也是前不久才宣佈支持閉包的,但是我們現在可以用匿名內部類來模擬閉包的實現。可惜模擬終歸模擬,會有許多的限制。那麼如果Java的程序員像用閉包的話該怎麼辦呢?其實不支持閉包也僅僅是Java語言,在Java虛擬機上已經有能夠讓我們隨心所欲使用這個工具的語言了——對,就是Groovy和Scala。當然還有JRuby和Jython。總之我們的選擇有很多,而我們要做的就是接受它們。


參考及引用資料:

  1. Bruce Tate:跨越邊界: 閉包 
  2. 李文浩:閉包的概念、形式與應用
  3. Martin Flower:Closure

文中大部分的代碼來自於以上文章。

另注:該文章屬於學習筆記類,很多東西並非作者原創,絕無侵犯版權之意,特此聲明。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章