在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是在支援頭等函式的程式語言中實現詞法繫結的一種技術。閉包在實現上是一個結構體,它儲存了一個函式(通常是其入口位址)和一個關聯的環境(相當於一個符號尋找表)。環境裡是若干對符號和值的對應關係,它既要包括約束變數(該函式內部繫結的符號),也要包括自由變數(在函式外部定義但在函式內被參照),有些函式也可能沒有自由變數。閉包跟函式最大的不同在於,當捕捉閉包的時候,它的自由變數會在捕捉時被確定,這樣即便脫離了捕捉時的上下文,它也能照常執行。捕捉時對於值的處理可以是值拷貝,也可以是名稱參照,這通常由語言設計者決定,也可能由使用者自行指定(如C++)。
概述
閉包的概念出現於60年代,最早實現閉包的程式語言是Scheme。之後,閉包被廣泛使用於函數式程式設計語言如ML語言和LISP。很多命令式程式語言也開始支援閉包。
在支援頭等函式的語言中,如果函式f內定義了函式g,那麼如果g存在自由變數,且這些自由變數沒有在編譯過程中被最佳化掉,那麼將產生閉包。
閉包和匿名函式經常被用作同義詞。但嚴格來說,匿名函式就是字面意義上沒有被賦予名稱的函式,而閉包則實際上是一個函式的實例,也就是說它是存在於主記憶體里的某個結構體。如果從實現上來看的話,匿名函式如果沒有捕捉自由變數,那麼它其實可以被實現為一個函式指標,或者直接行內到呼叫點,如果它捕捉了自由變數那麼它將是一個閉包;而閉包則意味著同時包括函式指標和環境兩個關鍵元素。在編譯最佳化當中,沒有捕捉自由變數的閉包可以被最佳化成普通函式,這樣就無需分配閉包結構體,這種編譯技巧被稱為函式躍升。
詞源
閉包的概念是在1960年代為採用lambda演算的表達式的機器求值而開發的,它首次在1970年於PAL程式語言中完全實現,用來支援詞法作用域的頭等函式[1]。
彼得·蘭丁(Peter Landin)在1964年將術語「閉包」定義為一種包含環境成分和控制成分的實體,用於在他的SECD機器上對表達式求值[2]。 Joel Moses認為是Landin發明了「閉包」這一術語,用來指代某些其開放繫結(自由變數)已經由其語法環境完成閉合(或者繫結)的lambda表達式,從而形成了閉合的表達式,或稱閉包。[3][4]。這一用法後來於1975年被Sussman和Steele在定義 Scheme語言的時候予以採納[5],並廣為流傳。
語意
閉包可以用來在一個函式與一組「私有」變數之間建立關聯關係。在給定函式被多次呼叫的過程中,這些私有變數能夠保持其永續性。變數的作用域僅限於包含它們的函式,因此無法從其它程式碼部分進行訪問。不過,變數的生存期是可以很長,在一次函式呼叫期間所建立所生成的值在下次函式呼叫時仍然存在。正因為這一特點,閉包可以用來完成資訊隱藏,並進而應用於需要狀態表達的某些程式設計範式中。
不過,用這種方式來使用閉包時,閉包不再具有參照透明性,因此也不再是純函式。即便如此,在某些非純函數式程式設計語言,例如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
取代了lambda
,Array.filter
方法[6]取代了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;
};
}
因為在這個例子中閉包已經超出了建立它的函式的範圍,所以變數f
和dx
將在函式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 提升」。例:
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){
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語言中,支援回呼函式的庫有時在註冊時需要兩個參數:一個函式指標,一個獨立的void*
指標用以儲存使用者資料。這樣的做法允許回呼函式恢復其呼叫時的狀態。這樣的慣用法在功能上類似於閉包,但語法上有所不同。
C語言 (使用LLVM編譯器或蘋果修改版的GCC)支援塊。閉包變數用__block標記。同時,這個擴充也可以應用到Objective-C與C++中。
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++早期標準允許通過多載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'的字符串
}
參考文獻
外部連結
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.