lua學習之閉包實現原理
感覺學習lua的過程中, 閉包的概念比較難以理解,這裏記錄下對閉包的學習。
閉包的概念
在Lua中,閉包(closure)是由一個函數和該函數會訪問到的非局部變量(或者是upvalue)組成的,其中非局部變量(non-local variable)是指不是在局部作用範圍內定義的一個變量,但同時又不是一個全局變量,主要應用在嵌套函數和匿名函數裏,因此若一個閉包沒有會訪問的非局部變量,那麽它就是通常說的函數。也就是說,在Lua中,函數是閉包一種特殊情況。在Lua中,函數是一種第一類型值(First-Class Value),它們具有特定的詞法域(Lexical Scoping)。
第一類型值表示函數與其他傳統類型的值(例如數字和字符串類型)具有相同的權利。即函數可以存儲在變量或table。
function foo(x) print(x) end
實質是等價於
foo = function (x) print(x) end
因此一個函數定義實質就是一條賦值語句,這條語句創建了一種類型為“函數”的值,並賦值給一個變量。可以將表達式function (x) <body> end 視為一種函數構造式,就像table的構造式{}一樣。
詞法域是指一個函數可以嵌套在另一個函數中,內部的函數可以訪問外部函數的變量的這樣一種特征。比如:
function f1(n) --函數參數n也是局部變量 local function f2() print(n) --引用外部函數的局部變量 end return f2 end g1 = f1(2015) g1() -- 打印出2015 g2 = f1(2016)
註意這裏的g1和g2的函數體相同(都是f1的內嵌函數f2的函數體),但打印值不同。這是因為創建這兩個閉包時,他們都擁有局部變量n的獨立實例。事實上,Lua編譯一個函數時,會為他生成一個原型(prototype),其中包含了函數體對應的虛擬機指令、函數用到的常量值(數,文本字符串等等)和一些調試信息。在運行時,每當Lua執行一個形如function...end 這樣的表達式時,他就會創建一個新的數據對象,其中包含了相應函數原型的引用及一個由所有upvalue引用組成的數組,而這個數據對象就稱為閉包。由此可見,函數是編譯期概念,是靜態的,而閉包是運行期概念,是動態的。g1和g2的值嚴格來說不是函數而是閉包,並且是兩個不相同的閉包,而每個閉包能保有自己的upvalue值,所以g1和g2打印出的結果當然就不相同了。
這裏的函數f2可以訪問參數n,而n是外部函數f1的局部變量。在f2中,變量n即不是全局變量也不是局部變量,將其稱為一個非局部變量(non-local variable)或upvalue。upvalue實際指的是變量而不是值,這些變量可以在內部函數之間共享,即upvalue提供一種閉包之間共享數據的方法,比如:
function Create(n) local function foo1() print(n) end local function foo2() n = n + 10 end return foo1,foo2 end f1,f2 = Create(2015) f1() -- 打印2015 f2() f1() -- 打印2025 f2() f1() -- 打印2035
註意上面的例子中,閉包f1和f2共享同一個upvalue了,這是因為當Lua發現兩個閉包的upvalue指向的是當前堆棧上的相同變量時,會聰明地只生成一個拷貝,然後讓這兩個閉包共享該拷貝,這樣任一個閉包對該upvalue進行修改都會被另一個探知。
閉包的實現原理
當Lua編譯一個函數時,它會生成一個原型(prototype),原型中包括函數的虛擬機指令、函數中的常量(數值和字符串等)和一些調試信息。在任何時候只要Lua執行一個function .. end表達時,它都會創建一個新的閉包(closure)。每個閉包都有一個相應函數原型的引用以及一個數組,數組中每個元素都是一個對upvalue的引用,可以通過該數組來訪問外部的局部變量(outer local variables)。值得註意的是,在Lua 5.2之前,閉包中還包括一個對環境(environment)的引用,環境實質就是一個table,函數可以在該表中索引全局變量,從Lua 5.2開始,取消了閉包中的環境,而引入一個變量_ENV來設置閉包環境。由此可見,函數是編譯期概念,是靜態的,而閉包是運行期概念,是動態的。
lua學習之閉包實現原理