闭包_(计算机科学)

在電腦科學中，**閉包**（英語：Closure），又稱**詞法閉包**（Lexical Closure）或**函式閉包**（function closures），是在支援頭等函式的程式語言中實現詞法繫結的一種技術。閉包在實現上是一個結構體，它儲存了一個函式（通常是其入口位址）和一個關聯的環境（相當於一個符號尋找表）。環境裡是若干對符號和值的對應關係，它既要包括約束變數（該函式內部繫結的符號），也要包括自由變數（在函式外部定義但在函式內被參照），有些函式也可能沒有自由變數。閉包跟函式最大的不同在於，當捕捉閉包的時候，它的自由變數會在捕捉時被確定，這樣即便脫離了捕捉時的上下文，它也能照常執行。捕捉時對於值的處理可以是值拷貝，也可以是名稱參照，這通常由語言設計者決定，也可能由使用者自行指定（如 C++）。

## 概述

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

在支援頭等函式的語言中，如果函式 f 內定義了函式 g，那麼如果 g 存在自由變數，且這些自由變數沒有在編譯過程中被最佳化掉，那麼將產生閉包。

閉包和匿名函式經常被用作同義詞。但嚴格來說，匿名函式就是字面意義上沒有被賦予名稱的函式，而閉包則實際上是一個函式的實例，也就是說它是存在於記憶體里的某個結構體。如果從實現上來看的話，匿名函式如果沒有捕捉自由變數，那麼它其實可以被實現為一個函式指標，或者直接行內到呼叫點，如果它捕捉了自由變數那麼它將是一個閉包；而閉包則意味著同時包括函式指標和環境兩個關鍵元素。在編譯最佳化當中，沒有捕捉自由變數的閉包可以被最佳化成普通函式，這樣就無需分配閉包結構體，這種編譯技巧被稱為函式躍升 （頁面存檔備份，存於網際網路檔案館）。

## 詞源

閉包的概念是在 1960 年代為採用 lambda 演算的表達式的機器求值而開發的，它首次在 1970 年於 PAL 程式語言中完全實現，用來支援詞法作用域的頭等函式。

彼得・蘭丁（Peter Landin）在 1964 年將術語「閉包」定義為一種包含環境成分和控制成分的實體，用於在他的 SECD 機器上對表達式求值。Joel Moses 認為是 Landin 發明了「閉包」這一術語，用來指代某些其開放繫結（自由變數）已經由其語法環境完成閉合（或者繫結）的 lambda 表達式，從而形成了閉合的表達式，或稱閉包。。這一用法後來於 1975 年被 Sussman 和 Steele 在定義 Scheme 語言的時候予以採納，並廣為流傳。

## 語意

### 閉包和狀態表達

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

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

### 閉包和頭類函式

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

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

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

下面是用 ECMAScript （JavaScript）寫的同一個例子：

這裡，關鍵字 `function` 取代了 `lambda`，`Array.filter` 方法取代了 `filter` 函式，但兩段代碼的功能是一樣的。

一個函式可以建立一個閉包並返回它，如下述 JavaScript 例子：

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

## 閉包的用途

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

* 閉包可以用來實現物件系統。

## 閉包的實現

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

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

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

## 各種語言中（類似）閉包的結構

### C 語言的回呼函式

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

### gcc 對 C 語言的擴充

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

### C 語言擴充：Blocks

C 語言 (使用 LLVM 編譯器或蘋果修改版的 GCC) 支援塊。閉包變數用 \_\_block 標記。同時，這個擴充也可以應用到 Objective-C 與 C++ 中。

### C++ 函式物件

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

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

## 參考文獻

## 外部連結

* 跨越邊界：閉包 (IBM DeveloperWorks) （頁面存檔備份，存於網際網路檔案館）
* 閉包的概念、形式與應用 (IBM DeveloperWorks) （頁面存檔備份，存於網際網路檔案館）
* 深入理解 Javascript 閉包 (closure) - Felix Woo