閉包的概念

什麼是閉包?

閉包並非什麼新奇的概念,它早在高級語言開始發展的年代就產生了。閉包(Closure)是詞法閉包(Lexical Closure)的簡稱。對閉包的具體定義有不少種說法,這些說法大致能夠分爲兩類:javascript

  • 一種說法認爲閉包是符合必定條件的函數,好比參考資源中這樣定義閉包:閉包是在其詞法上下文中引用了自由變量(指除局部變量之外的變量)的函數。
  • 另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。有這樣的的定義:在實現深約束(英文原詞是 binding,也有人把它翻譯爲綁定)時,須要建立一個能顯式表示引用環境的東西,並將它與相關的子程序捆綁在一塊兒,這樣捆綁起來的總體被稱爲閉包。

這兩種定義在某種意義上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。雖然有些咬文嚼字,但能夠確定第二種說法更確切。閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行的代碼,這些代碼在函數被定義後就肯定了,不會在執行時發生變化,因此一個函數只有一個實例。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。所謂引用環境是指在程序執行中的某個點全部處於活躍狀態的約束所組成的集合。其中的約束是指一個變量的名字和其所表明的對象之間的聯繫。那麼爲何要把引用環境與函數組合起來呢?這主要是由於在支持嵌套做用域的語言中,有時不能簡單直接地肯定函數的引用環境。這樣的語言通常具備這樣的特性:html

  • 函數是一階值(First-class value),即函數能夠做爲另外一個函數的返回值或參數,還能夠做爲一個變量的值。
  • 函數能夠嵌套定義,即在一個函數內部能夠定義另外一個函數。

這些概念上的解釋很難理解,顯然一個實際的例子更能說明問題。Lua 語言的語法比較接近僞代碼,咱們來看一段 Lua 的代碼:java

function make_counter() local count = 0 function inc_count() count = count + 1 return count end return inc_countendc1 = make_counter()c2 = make_counter()print(c1())print(c2())

 

在這段程序中,函數 inc_count 定義在函數 make_counter 內部,並做爲 make_counter 的返回值。變量 count 不是 inc_count 內的局部變量,按照最內嵌套做用域的規則,inc_count 中的 count 引用的是外層函數中的局部變量 count。接下來的代碼中兩次調用 make_counter() ,並把返回值分別賦值給 c1 和 c2 ,而後又依次打印調用 c1 和 c2 所獲得的返回值。 
這裏存在一個問題,當調用 make_counter 時,在其執行上下文中生成了局部變量 count 的實例,因此函數 inc_count 中的 count 引用的就是這個實例。可是 inc_count 並無在此時被執行,而是做爲返回值返回。當 make_counter 返回後,其執行上下文將失效,count 實例的生命週期也就結束了,在後面對 c1 和 c2 調用實際是對 inc_count 的調用,而此處並不在 count 的做用域中,這看起來是沒法正確執行的。 
上面的例子說明了把函數做爲返回值時須要面對的問題。當把函數做爲參數時,也存在類似的問題。下面的例子演示了把函數做爲參數的狀況。python

function do10times(fn) for i = 0,9 do fn(i) end end sum = 0 function addsum(i) sum = sum + i end do10times(addsum) print(sum)

 

這裏咱們看到,函數 addsum 被傳遞給函數 do10times,被並在 do10times 中被調用10次。不難看出 addsum 實際的執行點在 do10times 內部,它要訪問非局部變量 sum,而 do10times 並不在 sum 的做用域內。這看起來也是沒法正常執行的。 
這兩種狀況所面臨的問題實質是相同的。在這樣的語言中,若是按照做用域規則在執行時肯定一個函數的引用環境,那麼這個引用環境可能和函數定義時不一樣。要想使這兩段程序正常執行,一個簡單的辦法是在函數定義時捕獲當時的引用環境,並與函數代碼組合成一個總體。當把這個總體看成函數調用時,先把其中的引用環境覆蓋到當前的引用環境上,而後執行具體代碼,並在調用結束後恢復原來的引用環境。這樣就保證了函數定義和執行時的引用環境是相同的。這種由引用環境與函數代碼組成的實體就是閉包。固然若是編譯器或解釋器可以肯定一個函數在定義和運行時的引用環境是相同的(一個函數中沒有自由變量時,引用環境不會發生變化),那就沒有必要把引用環境和代碼組合起來了,這時只須要傳遞普通的函數就能夠了。如今能夠得出這樣的結論:閉包不是函數,只是行爲和函數類似,不是全部被傳遞的函數都須要轉化爲閉包,只有引用環境可能發生變化的函數才須要這樣作。 
再次觀察上面兩個例子會發現,代碼中並無經過名字來調用函數 inc_count 和 addsum,因此他們根本不須要名字。以第一段代碼爲例,它能夠重寫成下面這樣:git

function make_counter() local count = 0 return function() count = count + 1 return count end end c1 = make_counter() c2 = make_counter() print(c1()) print(c2())

 

這裏使用了匿名函數。使用匿名函數能使代碼獲得簡化,同時咱們也沒必要挖空心思地去給一個不須要名字的函數取名字了。 
一個編程語言須要哪些特性來支持閉包呢,下面列出一些比較重要的條件:程序員

  • 函數是一階值;
  • 函數能夠嵌套定義;
  • 能夠捕獲引用環境,並把引用環境和函數代碼組成一個可調用的實體;
  • 容許定義匿名函數;

這些條件並非必要的,但具有這些條件能說明一個編程語言對閉包的支持較爲完善。另外須要注意,有些語言使用與函數定義不一樣的語法來定義這種能被傳遞的」函數」,如 Ruby 中的 Block。這其實是語法糖,只是爲了更容易定義匿名函數而已,本質上沒有區別。 
借用一個很是好的說法來作個總結:對象是附有行爲的數據,而閉包是附有數據的行爲。github

閉包的表現形式

雖然創建在類似的思想之上,各類語言所實現的閉包卻有着不一樣的表現形式,下面咱們來看一下閉包在一些經常使用語言中的表現形式。編程

JavaScript 中的閉包

JavaScript(ECMAScript)不是通用編程語言,但卻擁有較大的用戶羣體,而 Ajax 的流行也使更多的人關注 JavaScript。雖然在進行 DOM 操做時容易引起循環引用問題,但 JavaScript 語言自己對閉包的支持仍是很好的,下面是一個簡單的例子:數組

function addx(x) { return function(y) {return x+y;}; } add8 = addx(8); add9 = addx(9); alert(add8(100)); alert(add9(100));

 

 

Ruby 中的閉包

隨着 Ruby on Rails 的走紅,Ruby 無疑是時下煊赫一時的語言之一,Ruby 吸收了不少其餘語言的優勢,是很是優秀的語言,從這一點來看,很難說清是 Rails 成就了 Ruby 仍是 Ruby 成就了 Rails。 
Ruby 使用 Block 來定義閉包,Block 在 Ruby 中十分重要,幾乎處處均可以看到它的身影,下面的代碼就展現了一個 Block:ruby

sum = 0 10.times{|n| sum += n} print sum

 

10.times 表示調用對象10的 times 方法(在Ruby中一切都是對象,數字也是對象),緊跟在這個調用後面的大括號裏面的部分就是Block。所謂 Block 是指緊跟在函數調用以後用大括號或 do/end 括起來的代碼,Block 的開始部分(左大括號或 do)必須和函數調用在同一行。Block 也能夠接受參數,參數列表必須用兩個豎槓括起來放在最前面。Block 會被做爲它前面的函數調用的參數,而在這個函數中可使用關鍵字 yield 來調用該 Block。在這個例子中,10.times 會以數字0到9爲參數調用 Block 10次。 
Block 實際上就是匿名函數,它能夠被調用,能夠捕獲上下文。因爲語法上要求 Block 必須出如今函數調用的後面,因此 Block 不能直接做爲函數的的返回值。要想從一個函數中返回 Block,必須使用 proc 或 lambda 函數把 Block 轉化爲對象才行。

Python 中的閉包

Python 因其簡單易學、功能強大而擁有不少擁護者,不少企業和組織在使用這種語言。Python 使用縮進來區分做用域的作法也十分有特色。下面是一個 Python 的例子:

def addx(x): def adder (y): return x + y return adder add8 = addx(8) add9 = addx(9) print add8(100) print add9(100)

 

在 Python 中使用 def 來定義函數時,是必須有名字的,要想使用匿名函數,則須要使用lambda 語句,像下面的代碼這樣:

def addx(x): return lambda y: x + y add8 = addx(8) add9 = addx(9) print add8(100) print add9(100)

 

Perl 中的閉包

Perl 是老牌文本處理語言了,在 WEB 開發方面也有一席之地。不過 Perl6 的開發進行比較慢,也許一些用戶開始轉投其它語言了。下面是一個 Perl 的例子:

sub addx { my $x = shift; return sub { shift() + $x }; } $add8 = addx(8); $add9 = addx(9); print $add8->(100); print $add9->(100);

 

閉包的應用

閉包能夠用優雅的方式來處理一些棘手的問題,有些程序員聲稱沒有閉包簡直就活不下去了。這雖然有些誇張,卻從側面說明閉包有着強大的功能。下面列舉了一些閉包應用。

增強模塊化

閉包有益於模塊化編程,它能以簡單的方式開發較小的模塊,從而提升開發速度和程序的可複用性。和沒有使用閉包的程序相比,使用閉包可將模塊劃分得更小。好比咱們要計算一個數組中全部數字的和,這隻須要循環遍歷數組,把遍歷到的數字加起來就好了。若是如今要計算全部元素的積呢?要打印全部的元素呢?解決這些問題都要對數組進行遍歷,若是是在不支持閉包的語言中,咱們不得不一次又一次重複地寫循環語句。而這在支持閉包的語言中是沒必要要的。

抽象

閉包是數據和行爲的組合,這使得閉包具備較好抽象能力,下面的代碼經過閉包來模擬面向對象編程。函數 make_stack 用來生成 stack 對象,它的返回值是一個閉包,這個閉包做爲一個 Dispatcher,當以 「push」 或 「pop」 爲參數調用時,返回一個與函數 push 或 pop 相關聯的閉包,進而能夠操做 data 中的數據。

簡化代碼

咱們來考慮一個常見的問題。在一個窗口上有一個按鈕控件,當點擊按鈕時會產生事件,若是咱們選擇在按鈕中處理這個事件,那就必須在按鈕控件中保存處理這個事件時須要的各個對象的引用。另外一種選擇是把這個事件轉發給父窗口,由父窗口來處理這個事件,或是使用監聽者模式。不管哪一種方式,編寫代碼都不太方便,甚至要藉助一些工具來幫助生成事件處理的代碼框架。用閉包來處理這個問題則比較方便,能夠在生成按鈕控件的同時就寫下事件處理代碼。

更多

閉包的應用遠不止這些,這裏列舉的只能算是冰山一角而已,而且更多的用法還不斷髮現中。要想了解更多的用法,多看一些代碼應該是個不錯的選擇。

總結

閉包能優雅地解決不少問題,不少主流語言也順應潮流,已經或將要引入閉包支持。相信閉包會成爲更多人愛不釋手的工具。閉包起源於函數語言,也許掌握一門函數語言是理解閉包的最佳途徑,並且經過學習函數語言能夠了解不一樣的編程思想,有益於寫出更好的程序。

 

 

 

 

 

閉包(closure)是函數式編程的重要的語法結構。函數式編程是一種編程範式 (而面向過程編程和麪向對象編程也都是編程範式)。在面向過程編程中,咱們見到過函數(function);在面向對象編程中,咱們見過對象(object)。函數和對象的根本目的是以某種邏輯方式組織代碼,並提升代碼的可重複使用性(reusability)。閉包也是一種組織代碼的結構,它一樣提升了代碼的可重複使用性。

不一樣的語言實現閉包的方式不一樣。Python以函數對象爲基礎,爲閉包這一語法結構提供支持的 (咱們在特殊方法與多範式中,已經屢次看到Python使用對象來實現一些特殊的語法)。Python一切皆對象,函數這一語法結構也是一個對象。在函數對象中,咱們像使用一個普通對象同樣使用函數對象,好比更改函數對象的名字,或者將函數對象做爲參數進行傳遞。

 

函數對象的做用域

和其餘對象同樣,函數對象也有其存活的範圍,也就是函數對象的做用域。函數對象是使用def語句定義的,函數對象的做用域與def所在的層級相同。好比下面代碼,咱們在line_conf函數的隸屬範圍內定義的函數line,就只能在line_conf的隸屬範圍內調用。

line函數定義了一條直線(y = 2x + 1)。能夠看到,在line_conf()中能夠調用line函數,而在做用域以外調用line將會有下面的錯誤:

NameError: name ‘line’ is not defined

說明這時已經在做用域以外。

 

一樣,若是使用lambda定義函數,那麼函數對象的做用域與lambda所在的層級相同。

 

閉包

函數是一個對象,因此能夠做爲某個函數的返回結果。

上面的代碼能夠成功運行。line_conf的返回結果被賦給line對象。上面的代碼將打印11。

 

若是line()的定義中引用了外部的變量,會發生什麼呢?

咱們能夠看到,line定義的隸屬程序塊中引用了高層級的變量b,但b信息存在於line的定義以外 (b的定義並不在line的隸屬程序塊中)。咱們稱b爲line的環境變量。事實上,line做爲line_conf的返回值時,line中已經包括b的取值(儘管b並不隸屬於line)。

上面的代碼將打印25,也就是說,line所參照的b值是函數對象定義時可供參考的b值,而不是使用時的b值。

 

一個函數和它的環境變量合在一塊兒,就構成了一個閉包(closure)。在Python中,所謂的閉包是一個包含有環境變量取值的函數對象。環境變量取值被保存在函數對象的__closure__屬性中。好比下面的代碼:

__closure__裏包含了一個元組(tuple)。這個元組中的每一個元素是cell類型的對象。咱們看到第一個cell包含的就是整數15,也就是咱們建立閉包時的環境變量b的取值。

 

下面看一個閉包的實際例子:

這個例子中,函數line與環境變量a,b構 成閉包。在建立閉包的時候,咱們經過line_conf的參數a,b說明了這兩個環境變量的取值,這樣,咱們就肯定了函數的最終形式(y = x + 1和y = 4x + 5)。咱們只須要變換參數a,b,就能夠得到不一樣的直線表達函數。由此,咱們能夠看到,閉包也具備提升代碼可複用性的做用。

若是沒有閉包,咱們須要每次建立直線函數的時候同時說明a,b,x。這樣,咱們就須要更多的參數傳遞,也減小了代碼的可移植性。利用閉包,咱們實際上建立了泛函。line函數定義一種普遍意義的函數。這個函數的一些方面已經肯定(必須是直線),但另外一些方面(好比a和b參數待定)。隨後,咱們根據line_conf傳遞來的參數,經過閉包的形式,將最終函數肯定下來。

 

閉包與並行運算

閉包有效的減小了函數所需定義的參數數目。這 對於並行運算來講有重要的意義。在並行運算的環境下,咱們可讓每臺電腦負責一個函數,而後將一臺電腦的輸出和下一臺電腦的輸入串聯起來。最終,咱們像流 水線同樣工做,從串聯的電腦集羣一端輸入數據,從另外一端輸出數據。這樣的情境最適合只有一個參數輸入的函數。閉包就能夠實現這一目的。

並行運算正稱爲一個熱點。這也是函數式編程又 熱起來的一個重要緣由。函數式編程早在1950年代就已經存在,但應用並不普遍。然而,咱們上面描述的流水線式的工做並行集羣過程,正適合函數式編程。由 於函數式編程這一自然優點,愈來愈多的語言也開始加入對函數式編程範式的支持。

相關文章
相關標籤/搜索