閉包 (計算機科學)

在計算機科學中，閉包，又稱詞法閉包，是在支援頭等函式的程式語言中實現詞法繫結的一種技術。閉包在實現上是一個結構體，它儲存了一個函式。概述.閉包的概念出現於60年代，最早實現閉包的程式語言是Scheme。之後，閉包被廣泛使用於函數語言程式設計語言如ML語言和LISP。很多命令式程式語言也開始支援閉包。在支援頭等函式的語言中，如果函式f內定義了函式g，那麼如果g存在自由變數，且這些自由變數沒有在編譯過程中被最佳化掉，那麼將產生閉包。閉包和匿名函式經常被用作同義詞。但嚴格來說，匿名函式就是字面意義上沒有被賦予名稱的函式，而閉包則實際上是一個函式的例項，也就是說它是存在於記憶體裡的某個結構體。如果從實現上來看的話，匿名函式如果沒有捕捉自由變數，那麼它其實可以被實現為一個函式指標，或者直接內聯到呼叫點，如果它捕捉了自由變數那麼它將是一個閉包；而閉包則意味著同時包括函式指標和環境兩個關鍵元素。在編譯最佳化當中，沒有捕捉自由變數的閉包可以被最佳化成普通函式，這樣就無需分配閉包結構體，這種編譯技巧被稱為。詞源.閉包的概念是在1960年代為採用lambda演算的表示式的機器求值而開發的，它首次在1970年於PAL程式語言中完全實現，用來支援詞法作用域的頭等函式。彼得·蘭丁在1964年將術語“閉包”定義為一種包含環境成分和控制成分的實體，用於在他的SECD機器上對錶達式求值。認為是Landin發明了“閉包”這一術語，用來指代某些其開放繫結的lambda表示式，從而形成了閉合的表示式，或稱閉包。。這一用法後來於1975年被Sussman和Steele在定義 Scheme語言的時候予以採納，並廣為流傳。語義.閉包和狀態表達.閉包可以用來在一個函式與一組“私有”變數之間建立關聯關係。在給定函式被多次呼叫的過程中，這些私有變數能夠保持其永續性。變數的作用域僅限於包含它們的函式，因此無法從其它程式程式碼部分進行訪問。不過，變數的生存期是可以很長，在一次函式呼叫期間所建立所生成的值在下次函式呼叫時仍然存在。正因為這一特點，閉包可以用來完成資訊隱藏，並進而應用於需要狀態表達的某些程式設計範型中。不過，用這種方式來使用閉包時，閉包不再具有，因此也不再是純函式。即便如此，在某些非純函數語言程式設計語言，例如Scheme中，閉包還是得到了廣泛的使用。閉包和頭類函式.典型的支援閉包的語言中，通常將函式當作頭等函式——在這些語言中，函式可以被當作引數傳遞、也可以作為函式返回值、繫結到變數名、就像字串、整數等簡單型別。例如以下Scheme程式碼：(filter(lambda (book)(&gt;= (book-sales book) threshold))book-list))在這個例子中，lambda表示式codice_1出現在函式codice_2中。當這個lambda表示式被執行時，Scheme創造了一個包含此表示式以及對codice_3變數的引用的閉包，其中codice_3變數在lambda表示式中是自由變數。這個閉包接著被傳遞到codice_5函式。這個函式的功能是重複呼叫這個閉包以判斷哪些書需要增加到列表哪些書需要丟棄。因為閉包中引用了變數codice_3，所以它在每次被codice_5呼叫時都可以使用這個變數，雖然codice_5可能定義在另一個檔案中。下面是用ECMAScript 寫的同一個例子：// Return a list of all books with at least 'threshold' copies sold.function bestSellingBooks(threshold) {return bookList.filter(這裡，關鍵字codice_9取代了codice_10，codice_11方法取代了codice_5函式，但兩段程式碼的功能是一樣的。一個函式可以建立一個閉包並返回它，如下述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;}因為在這個例子中閉包已經超出了建立它的函式的範圍，所以變數codice_13和codice_14將在函式codice_15返回後繼續存在。在沒有閉包的語言中，變數的生命週期只限於建立它的環境。但在有閉包的語言中，只要有一個閉包引用了這個變數，它就會一直存在。清理不被任何函式引用的變數的工作通常由垃圾回收完成，但對於 C++ 這種沒有垃圾收集的語言而言也不是難事——透過一些細緻而瑣碎的步驟。閉包的用途.(set! foo (lambda (msg) (set! secret-message msg)))(set! bar (lambda () secret-message)))(display (bar)) ; prints "none"(display (bar)) ; prints "meet me by the docks at midnight"閉包的實現.典型實現方式是定義一個特殊的資料結構，儲存了函式地址指標與閉包建立時的函式的詞法環境表示。使用函式呼叫棧的語言實現閉包比較困難，因而這也說明了為什麼大多數實現閉包的語言是基於垃圾收集機制——當然，不使用垃圾收集也可以做到。閉包的實現與函式物件很相似。透過將自由變數放進引數表、並擴大函式名字的作用域，可以把一個閉包 / 匿名 / 內部函式變成一個普通的函式，這叫做“”。例：void G(void){const std::wstring wstr=L"Hello, world!";std::functionwchar_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::functionwchar_t{return wstr[ui%wstr.length()];std::wcout«fn(3, wstr)«std::endl;//'l'//現在 fn 中沒有自由變量了。把這個匿名函式取個名之後放到全域性命名空間裡：wchar_t fn(size_t ui, const std::wstring &amp;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語言的回撥函式.在C語言中，支援回撥函式的庫有時在註冊時需要兩個引數：一個函式指標，一個獨立的codice_16指標用以儲存使用者資料。這樣的做法允許回撥函式恢復其呼叫時的狀態。這樣的慣用法在功能上類似於閉包，但語法上有所不同。gcc對C語言的擴充套件.gcc編譯器對C語言實現了一種閉包的程式特性。C語言擴充套件：Blocks.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++函式物件.C++早期標準允許透過過載codice_17來定義函式物件。這種物件的行為在某種程度上與函數語言程式設計語言中的函式類似。它們可以在執行時動態建立、儲存狀態，但是不能如閉包一般方便地隱式獲取區域性變數，並且有“專物專用”的繁瑣問題——對於每一段閉包程式碼都要單獨寫一個函式物件類。C++11標準已經支援了閉包，這是一種特殊的函式物件，由特殊的語言結構——lambda表示式自動構建。C++閉包中儲存了其程式碼內全部向外引用的變數的複製或引用。如果是對外界環境中的物件的引用，且閉包執行時該外界環境的變數已經不存在，那麼可導致未定義行為，因為C++並不擴充套件這些被引用的外界環境的變數的生命期。示例程式碼如下：void foo(string myname) {typedef vector names;int y;names n;names::iterator i =find_if(n.begin(), n.end(), [&amp;](const string&amp; s){return s != myname &amp;&amp; s.size() &gt; y;});// 'i' 現在是'n.end()'或指向'n'中第一個// 不等於'myname'且長度大於'y'的字串