本頁使用了標題或全文手工轉換

閉包 (電腦科學)

維基百科,自由的百科全書
跳到: 導覽搜尋

電腦科學中,閉包英語:Closure),又稱詞法閉包Lexical Closure)或函數閉包function closures),是參照了自由變數的函數。這個被參照的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的參照環境組合而成的實體。閉包在執行時可以有多個例項,不同的參照環境和相同的函數組合可以產生不同的例項。

閉包的概念出現於60年代,最早實現閉包的程式語言是Scheme。之後,閉包被廣泛使用於函數語言程式設計語言如ML語言LISP。很多命令式程式語言也開始支援閉包。

在一些語言中,在函數中可以(巢狀)定義另一個函數時,如果內部的函數參照了外部的函數的變數,則可能產生閉包。執行時,一旦外部的 函數被執行,一個閉包就形成了,閉包中包含了內部函數的代碼,以及所需外部函數中的變數的參照。其中所參照的變數稱作上值(upvalue)。

閉包一詞經常和匿名函數混淆。這可能是因為兩者經常同時使用,但是它們是不同的概念。

詞源[編輯]

彼得·蘭丁 在1964年將術語 閉包 定義為一種包含 環境成分控制成分的實體,用於在他的SECD 機器上對表達式求值。[1] Joel Moses 認為是 Landin 發明了 閉包 這一術語,用來指代某些其開放繫結(自由變數)已經由其語法環境完成閉合(或者繫結)的 lambda 運算式,從而形成了 閉合的運算式,或稱閉包。[2][3] 這一用法後來於 1975 年被 SussmanSteele 在定義 Scheme 語言的時候予以採納。[4] 並廣為流傳。

語意[編輯]

閉包和狀態表達[編輯]

閉包可以用來在一個函數與一組「私有」變數之間建立關聯關係。在給定函數被多次呼叫的過程中,這些私有變數能夠保持其永續性。變數的作用域僅限於包含它們的函數,因此無法從其它程式代碼部分進行存取。不過,變數的生存期是可以很長,在一次函數呼叫期間所建立所生成的值在下次函數呼叫時仍然存在。正因為這一特點,閉包可以用來完成資訊隱藏,並進而應用於需要狀態表達的某些編程範型中。

不過,用這種方式來使用閉包時,閉包不再具有參照透明性,因此也不再是純函數。即便如此,在某些「近似於函數語言程式設計語言」的語言,例如Scheme中,閉包還是得到了廣泛的使用。

閉包和第一類函數[編輯]

典型的支援閉包的語言中,通常將函數當作第一類物件——在這些語言中,函數可以被當作參數傳遞、也可以作為函數返回值、繫結到變數名、就像字串整數簡單類型。例如以下Scheme代碼:

; Return a list  of all books with at least THRESHOLD copies sold.
(define  (best-selling-books  threshold)
   (filter
    (lambda (book) (>= (book-sales book)  threshold))
    book-list))

在這個例子中,lambda運算式(lambda (book) (>= (book-sales book) threshold))出現在函數best-selling-books中。當這個lambda運算式被執行時,Scheme創造了一個包含此運算式以及對threshold變數的參照的閉包,其中threshold變數在lambda運算式中是自由變數

這個閉包接着被傳遞到filter函數。這個函數的功能是重複呼叫這個閉包以判斷哪些書需要增加到列表哪些書需要丟棄。因為閉包中參照了變數threshold,所以它在每次被filter呼叫時都可以使用這個變數,雖然filter可能定義在另一個檔案中。

下面是用ECMAScript (JavaScript)寫的同一個例子:

// Return a  list of all books with at least 'threshold' copies sold.
function  bestSellingBooks(threshold) {
  return bookList.filter(
      function  (book) { return book.sales >= threshold; }
    );
}

這裏,關鍵字function取代了lambdaArray.filter方法[5]取代了filter函數,但兩段代碼的功能是一樣的。

一個函數可以建立一個閉包並返回它,如下述JavaScript例子:

// Return a function that approximates the derivative of f
// using an interval of dx, which should be appropriately small.
function derivative(f,  dx) {
  return  function (x) {
    return (f(x + dx) - f(x)) / dx;
  };
}

因為在這個例子中閉包已經超出了建立它的函數的範圍,所以變數fdx將在函數derivative返回後繼續存在。在沒有閉包的語言中,變數的生命周期只限於建立它的環境。但在有閉包的語言中,只要有一個閉包參照了這個變數,它就會一直存在。清理不被任何函數參照的變數的工作通常由垃圾回收完成,但對於 C++ 這種沒有垃圾收集(起碼目前仍沒有一個為語言本身所認可的)的語言而言也不是難事——通過一些細緻而瑣碎的步驟。

閉包的用途[編輯]

  • 因為閉包只有在被呼叫時才執行操作(暫且不論用於生成這個閉包物件本身的開銷,比如 C++ 中按值捕獲意味着執行複製建構函式),即「惰性求值」,所以它可以被用來定義控制結構。例如:在Smalltalk語言中,所有的控制結構,包括分歧條件(if/then/else)和迴圈(while和for),都是通過閉包實現的。用戶也可以使用閉包定義自己的控制結構。
  • 多個函數可以使用一個相同的環境,這使得它們可以通過改變那個環境相互交流。比如在Scheme中:
(define foo #f)
(define bar #f)

(let ((secret-message "none"))
  (set! foo (lambda (msg) (set! secret-message msg)))
  (set! bar (lambda () secret-message)))

(display (bar)) ; prints "none"
(newline)
(foo "meet me by the docks at midnight")
(display (bar)) ; prints "meet me by the docks at midnight"

閉包的實現[編輯]

典型實現方式是定義一個特殊的數據結構,儲存了函數地址指標與閉包建立時的函數的詞法環境表示(那些非局部變數的繫結)。使用函數呼叫棧的語言實現閉包比較困難,因而這也說明了為什麼大多數實現閉包的語言是基於垃圾收集機制——當然,不使用垃圾收集也可以做到。

閉包的實現與函數物件很相似。

通過將自由變數放進參數列、並擴大函數名字的作用域,可以把一個閉包 / 匿名 / 內部函數變成一個普通的函數,這叫做「Lambda 提升英語Lambda lifting」。例:

void G(void){
    const std::wstring wstr=L"Hello, world!";
    std::function<wchar_t(size_t)> fn=[&wstr](size_t ui)->wchar_t{
        return wstr[ui%wstr.length()];
    };
    std::wcout<<fn(3)<<std::endl;//'l'
}
//那么 fn 是一个闭包,指向那个匿名函数。
//这里 wstr 是自由变量,首先将其放入参数表:
void G(void){
    const std::wstring wstr=L"Hello, world!";
    std::function<wchar_t(size_t, const std::wstring &)> fn=[](size_t ui, const std::wstring &wstr)->wchar_t{
        return wstr[ui%wstr.length()];
    };
    std::wcout<<fn(3, wstr)<<std::endl;//'l'
}
//现在 fn 中没有自由变量了。把这个匿名函数取个名之后放到全局命名空间里:
wchar_t fn(size_t ui, const std::wstring &wstr)->wchar_t{
    return wstr[ui%wstr.length()];
}
void G(void){
    const std::wstring wstr=L"Hello, world!";
    std::wcout<<fn(3, wstr)<<std::endl;//'l'
}
//这就把 fn“提升”成了一个普通的函数。

各種語言中(類似)閉包的結構[編輯]

C語言的回呼函式[編輯]

C語言中,支援回呼函式的庫有時在註冊時需要兩個參數:一個函數指標,一個獨立的void*指標用以儲存用戶數據。這樣的做法允許回呼函式恢復其呼叫時的狀態。這樣的慣用法在功能上類似於閉包,但語法上有所不同。

gcc對C語言的擴充功能[編輯]

gcc編譯器對C語言實現了一種閉包的程式特性。

C語言擴充功能:Blocks[編輯]

C語言 (使用LLVM編譯器或蘋果修改版的GCC)支援。閉包變數用__block標記。同時,這個擴充功能也可以應用到Objective-CC++中。

typedef int (^IntBlock)();

IntBlock downCounter(int start) {
	 __block int i = start;
	 return Block_copy( ^int() {
		 return i--;
	 });
 }

IntBlock f = downCounter(5);
printf("%d", f());
printf("%d", f());
printf("%d", f());
Block_release(f);

C++函數物件[編輯]

C++早期標準允許通過過載operator()來定義函數物件。這種物件的行為在某種程度上與函數語言程式設計語言中的函數類似。它們可以在執行時動態建立、儲存狀態,但是不能如閉包一般方便地隱式取得局部變數,並且有「專物專用」的繁瑣問題——對於每一段閉包代碼都要單獨寫一個函數物件類。

C++11標準已經支援了閉包,這是一種特殊的函數物件,由特殊的語言結構——lambda運算式自動構建。C++閉包中儲存了其代碼內全部向外參照的變數的拷貝或參照。如果是對外界環境中的物件的參照,且閉包執行時該外界環境的變數已經不存在(如在呼叫棧上已經展開),那麼可導致未定義行為,因為C++並不擴充功能這些被參照的外界環境的變數的生命期。範例代碼如下:

void foo(string myname) {
	typedef vector<string> names;
	int y;
	names n;
	// ...
	names::iterator i =
	 find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;});	
	// 'i' 现在是'n.end()'或指向'n'中第一个
	// 不等于'myname'且长度大于'y'的字符串
}

參考資料[編輯]

  1. ^ 彼得·蘭丁, The mechanical evaluation of expressions, 1964 
  2. ^ Joel Moses, The Function of FUNCTION in LISP, or Why the FUNARG Problem Should Be Called the Environment Problem (PDF), June 1970 [2009-10-27], AI Memo 199, A useful metaphor for the difference between FUNCTION and QUOTE in LISP is to think of QUOTE as a porous or an open covering of the function since free variables escape to the current environment. FUNCTION acts as a closed or nonporous covering (hence the term "closure" used by Landin). Thus we talk of "open" Lambda expressions (functions in LISP are usually Lambda expressions) and "closed" Lambda expressions. [...] My interest in the environment problem began while Landin, who had a deep understanding of the problem, visited MIT during 1966-67. I then realized the correspondence between the FUNARG lists which are the results of the evaluation of "closed" Lambda expressions in LISP and ISWIM's Lambda Closures. 
  3. ^ Åke Wikström. Functional Programming using Standard ML. 1987. ISBN 0-13-331968-7. The reason it is called a "closure" is that an expression containing free variables is called an "open" expression, and by associating to it the bindings of its free variables, you close it. 
  4. ^ Gerald Jay Sussman and Guy L. Steele, Jr., Scheme: An Interpreter for the Extended Lambda Calculus, December 1975, AI Memo 349 
  5. ^ array.filter. Mozilla Developer Center. 10 January 2010 [2010-02-09]. 
  6. ^ Re: FP, OO and relations. Does anyone trump the others?. 29 December 1999 [2008-12-23]. 

外部連結[編輯]