Lua中的函數是一階類型值(first-class value),定義函數就象建立普通類型值同樣(只不過函數類型值的數據主要是一條條指令而已),因此在函數體中仍然能夠定義函數。假設函數f2定義在函數f1中,那麼就稱f2爲f1的內嵌(inner)函數,f1爲f2的外包(enclosing)函數,外包和內嵌都具備傳遞性,即f2的內嵌必然是f1的內嵌,而f1的外包也必定是f2的外包。內嵌函數能夠訪問外包函數已經建立的全部局部變量,這種特性即是所謂的詞法定界(lexical scoping),而這些局部變量則稱爲該內嵌函數的外部局部變量(external local variable)或者upvalue(這個詞多少會讓人產生誤解,由於upvalue實際指的是變量而不是值)。試看以下代碼:
function f1(n)
-- 函數參數也是局部變量
local function f2()
print(n) -- 引用外包函數的局部變量
end
return f2
end
g1 = f1(1979)
g1() -- 打印出1979
g2 = f1(500)
g2() -- 打印出500
當執行完g1 = f1(1979)後,局部變量n的生命本該結束,但由於它已經成了內嵌函數f2(它又被賦給了變量g1)的upvalue,因此它仍然能以某種形式繼續「存活」下來,從而令g1()打印出正確的值。
可爲何g2與g1的函數體同樣(都是f1的內嵌函數f2的函數體),但打印值不一樣?這就涉及到一個至關重要的概念——閉包(closure)。事實上,Lua編譯一個函數時,會爲它生成一個原型(prototype),其中包含了函數體對應的虛擬機指令、函數用到的常量值(數,文本字符串等等)和一些調試信息。在運行時,每當Lua執行一個形如function...end 這樣的表達式時,它就會建立一個新的數據對象,其中包含了相應函數原型的引用、環境(environment,用來查找全局變量的表)的引用以及一個由全部upvalue引用組成的數組,而這個數據對象就稱爲閉包。因而可知,函數是編譯期概念,是靜態的,而閉包是運行期概念,是動態的。g1和g2的值嚴格來講不是函數而是閉包,而且是兩個不相同的閉包,而每一個閉包能夠保有本身的upvalue值,因此g1和g2打印出的結果固然就不同了。雖然閉包和函數是本質不一樣的概念,但爲了方便,且在不引發混淆的狀況下,咱們對它們不作區分。
程序員
用用簡單的話來講upvalue:(也許不太嚴格,只爲快速理解)
upvalue:嵌套函數的外部函數的局部變量 function func(a) -- 這個函數返回值是一個函數 return function () a = a + 1 -- 這裏能夠訪問外部函數func的局部變量a,這個變量a就是upvalue return a end end
閉包:一個匿名函數加上其可訪問的upvalue
使用upvalue很方便,但它們的語義也很微妙,須要引發注意。好比將f1函數改爲:
function f1(n)
local function f2()
print(n)
end
n = n + 10
return f2
end
g1 = f1(1979)
g1() -- 打印出1989
內嵌函數定義在n = n + 10這條語句以前,可爲何g1()打印出的倒是1989?upvalue實際是局部變量,而局部變量是保存在函數堆棧框架上(stack frame)的,因此只要upvalue尚未離開本身的做用域,它就一直生存在函數堆棧上。這種狀況下,閉包將經過指向堆棧上的upvalue的引用來訪問它們,一旦upvalue即將離開本身的做用域(這也意味着它立刻要從堆棧中消失),閉包就會爲它分配空間並保存當前的值,之後即可經過指向新分配空間的引用來訪問該upvalue。當執行到f1(1979)的n = n + 10時,閉包已經建立了,可是n並無離開做用域,因此閉包仍然引用堆棧上的n,當return f2完成時,n即將結束生命,此時閉包便將n(已是1989了)複製到本身管理的空間中以便未來訪問。弄清楚了內部的祕密後,運行結果就不難解釋了。
upvalue還能夠爲閉包之間提供一種數據共享的機制。試看下例:
function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end
f1,f2 = Create(1979)
f1() -- 打印1979
f2()
f1() -- 打印1989
f2()
f1() -- 打印1999
f1,f2這兩個閉包的原型分別是Create中的內嵌函數foo1和foo2,而foo1和foo2引用的upvalue是同一個,即Create的局部變量n。前面已說過,執行完Create調用後,閉包會把堆棧上n的值複製出來,那麼是否f1和f2就分別擁有一個n的拷貝呢?其實否則,當Lua發現兩個閉包的upvalue指向的是當前堆棧上的相同變量時,會聰明地只生成一個拷貝,而後讓這兩個閉包共享該拷貝,這樣任一個閉包對該upvalue進行修改都會被另外一個探知。上述例子很清楚地說明了這點:每次調用f2都將upvalue的值增長了10,隨後f1將更新後的值打印出來。upvalue的這種語義頗有價值,它使得閉包之間能夠不依賴全局變量進行通信,從而使代碼的可靠性大大提升。
閉包在建立之時其upvalue就已經不在堆棧上的狀況也有可能發生,這是由於內嵌函數能夠引用更外層外包函數的局部變量:
function Test(n)
local function foo()
local function inner1()
print(n)
end
local function inner2()
n = n + 10
end
return inner1,inner2
end
return foo
end
t = Test(1979)
f1,f2 = t()
f1() -- 打印1979
f2()
f1() -- 打印1989
g1,g2 = t()
g1() -- 打印1989
g2()
g1() -- 打印1999
f1() -- 打印1999
執行完t = Test(1979)後,Test的局部變量n就「死」了,因此當f1,f2這兩個閉包被建立時堆棧上根本找不到n的蹤跡,這叫它們如何取得n的值呢?呵呵,不要忘了Test函數的n不只僅是inner1和inner2的upvalue,同時它也是foo的upvalue。t = Test(1979)以後,t這個閉包必定已經把n妥善保存好了,以後f一、f2若是在當前堆棧上找不到n就會自動到它們的外包閉包(姑且這麼叫)的upvalue引用數組中去找,並把找到的引用值拷貝到本身的upvalue引用數組中。仔細觀察上述代碼,能夠斷定g1和g2與f1和f2共享同一個upvalue。這是爲何呢?其實,g1和g2與f1和f2都是同一個閉包(t)建立的,因此它們引用的upvalue(n)實際也是同一個變量,而剛纔描述的搜索機制則保證了最後它們的upvalue引用都會指向同一個地方。
Lua將函數作爲基本類型值並支持詞法定界的特性使得語言具備強大的抽象能力。而透徹認識函數、閉包和upvalue將幫助程序員善用這種能力。數組
更多:http://book.luaer.cn/_129.htm閉包
http://hi.baidu.com/gxvogbuwgucfsxe/item/26ae633a4e24f020b3c0c528框架