lua閉包

lua的閉包是個新概念,理解它須要個過程。今天在網上找了幾篇文章看,不錯,先記錄下。
1,lua閉包普通篇 http://hi.baidu.com/happynp/blog/item/b7736a1f7f65b3ffe0fe0b90.html
2,lua閉包文藝篇 http://www.ibm.com/developerworks/cn/linux/l-cn-closure/
3,lua閉包2B篇 http://www.cnblogs.com/ringofthec/archive/2010/11/05/luaClosure.html
———————————————————————————————————————————————javascript

  lua中的函數是一階類型值(first-class value),定義函數就象建立普通類型值相同(只不過函數類型值的數據主要是一條條指令而已),因此在函數體中仍然能定義函數。假設函數f2定義在函數f1中,那麼就稱f2爲f1的內嵌(inner)函數,f1爲f2的外包(enclosing)函數,外包和內嵌都具備傳遞性,即f2的內嵌必然是f1的內嵌,而f1的外包也必定是f2的外包。內嵌函數能訪問外包函數已建立的全部局部變量,這種特性即是所謂的詞法定界(lexical scoping),而這些局部變量則稱爲該內嵌函數的外部局部變量(external local variable)或upvalue。試看以下代碼:html

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很是方便,但他們的語義也很是微妙,須要引發注意。好比將f1函數改爲:java

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還能爲閉包之間提供一種數據共享的機制。試看下例:linux

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就已不在堆棧上的狀況也有可能發生,這是由於內嵌函數能引用更外層外包函數的局部變量:c++

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將幫助程式員善用這種能力。程序員

—————————————————————————————————————————————————————————
什麼是閉包?
  閉包並非什麼新奇的概念,它早在高級語言開始發展的年代就產生了。閉包(Closure)是詞法閉包(Lexical Closure)的簡稱。對閉包的具體定義有不少種說法,這些說法大致能夠分爲兩類:
  一種說法認爲閉包是符合必定條件的函數,好比參考資源中這樣定義閉包:閉包是在其詞法上下文中引用了自由變量的函數。
  另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。好比參考資源中就有這樣的的定義:在實現深約束時,須要建立一個能顯式表示引用環境的東西,並將它與相關的子程序捆綁在一塊兒,這樣捆綁起來的總體被稱爲閉包。
  這兩種定義在某種意義上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。雖然有些咬文嚼字,但能夠確定第二種說法更確切。閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行的代碼,這些代碼在函數被定義後就肯定了,不會在執行時發生變化,因此一個函數只有一個實例。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。所謂引用環境是指在程序執行中的某個點全部處於活躍狀態的約束所組成的集合。其中的約束是指一個變量的名字和其所表明的對象之間的聯繫。那麼爲何要把引用環境與函數組合起來呢?這主要是由於在支持嵌套做用域的語言中,有時不能簡單直接地肯定函數的引用環境。這樣的語言通常具備這樣的特性:
  函數是一階值(First-class value),即函數能夠做爲另外一個函數的返回值或參數,還能夠做爲一個變量的值。
  函數能夠嵌套定義,即在一個函數內部能夠定義另外一個函數。
這些概念上的解釋很難理解,顯然一個實際的例子更能說明問題。Lua 語言的語法比較接近僞代碼,咱們來看一段 Lua 的代碼:
清單 1. 閉包示例1 編程

  

  在這段程序中,函數 inc_count 定義在函數 make_counter 內部,並做爲 make_counter 的返回值。變量 count 不是 inc_count 內的局部變量,按照最內嵌套做用域的規則,inc_count 中的 count 引用的是外層函數中的局部變量 count。接下來的代碼中兩次調用 make_counter() ,並把返回值分別賦值給 c1 和 c2 ,而後又依次打印調用 c1 和 c2 所獲得的返回值。
  這裏存在一個問題,當調用 make_counter 時,在其執行上下文中生成了局部變量 count 的實例,因此函數 inc_count 中的 count 引用的就是這個實例。可是 inc_count 並無在此時被執行,而是做爲返回值返回。當 make_counter 返回後,其執行上下文將失效,count 實例的生命週期也就結束了,在後面對 c1 和 c2 調用實際是對 inc_count 的調用,而此處並不在 count 的做用域中,這看起來是沒法正確執行的。
  上面的例子說明了把函數做爲返回值時須要面對的問題。當把函數做爲參數時,也存在類似的問題。下面的例子演示了把函數做爲參數的狀況。
清單 2. 閉包示例2 數組

function do5times(func)
    local total = 0
    for i=0, 5 do
        total = func(i)
    end
    return total
end

function aa()
    local sum = 0
    function addsum(i)
        sum = sum + i
        return sum
    end
    return addsum
end

s = aa()
s1 = aa()
print(do5times(s))
print(do5times(s1))

  這裏咱們看到,函數 addsum 被傳遞給函數 do5times,被並在 do5times 中被調用5次。不難看出 addsum 實際的執行點在 do5times 內部,它要訪問非局部變量 sum,而 do5times 並不在 sum 的做用域內。這看起來也是沒法正常執行的。
  這兩種狀況所面臨的問題實質是相同的。在這樣的語言中,若是按照做用域規則在執行時肯定一個函數的引用環境,那麼這個引用環境可能和函數定義時不一樣。要想使這兩段程序正常執行,一個簡單的辦法是在函數定義時捕獲當時的引用環境,並與函數代碼組合成一個總體。當把這個總體看成函數調用時,先把其中的引用環境覆蓋到當前的引用環境上,而後執行具體代碼,並在調用結束後恢復原來的引用環境。這樣就保證了函數定義和執行時的引用環境是相同的。這種由引用環境與函數代碼組成的實體就是閉包。固然若是編譯器或解釋器可以肯定一個函數在定義和運行時的引用環境是相同的,那就沒有必要把引用環境和代碼組合起來了,這時只須要傳遞普通的函數就能夠了。如今能夠得出這樣的結論:閉包不是函數,只是行爲和函數類似,不是全部被傳遞的函數都須要轉化爲閉包,只有引用環境可能發生變化的函數才須要這樣作。
  再次觀察上面兩個例子會發現,代碼中並無經過名字來調用函數 inc_count 和 addsum,因此他們根本不須要名字。以第一段代碼爲例,它能夠重寫成下面這樣:
清單 3. 閉包示例3 閉包

function make_counter()  
 local count = 0  
 return function()  
 count = count + 1  
 return count  
 end  
end  
  
c1 = make_counter()  
c2 = make_counter()  
print(c1())  
print(c2())  

這裏使用了匿名函數。使用匿名函數能使代碼獲得簡化,同時咱們也沒必要挖空心思地去給一個不須要名字的函數取名字了。
上面簡單地介紹了閉包的原理,更多的閉包相關的概念和理論請參考參考資源中的"名字,做用域和約束"一章。
一個編程語言須要哪些特性來支持閉包呢,下面列出一些比較重要的條件
  1. 函數是一階值;
  2. 函數能夠嵌套定義;
  3. 能夠捕獲引用環境,並把引用環境和函數代碼組成一個可調用的實體;
  4. 容許定義匿名函數;
  這些條件並非必要的,但具有這些條件能說明一個編程語言對閉包的支持較爲完善。另外須要注意,有些語言使用與函數定義不一樣的語法來定義這種能被傳遞的"函數",如 Ruby 中的 Block。這其實是語法糖,只是爲了更容易定義匿名函數而已,本質上沒有區別。
借用一個很是好的說法來作個總結:對象是附有行爲的數據,而閉包是附有數據的行爲
——————————————————————————————————————————————————————app

lua中有兩種閉包, c閉包和lua閉包

兩種閉包的公共部分:

#define ClosureHeader CommonHeader;     
lu_byte isC;         /*是不是C閉包*/     
lua_byte nupvalues;  /*upval的個數*/ 
GCObject* gclist;       

struct Table env   /* 閉包的env, set/getenv就是操縱的它 */

C閉包的結構:

struct CClosure{  
             ClosureHeader;  
             lua_CFunction f;  
             TValue upvalue[1];  
        }

結構比較簡單, f是一個知足 int lua_func(lua_State*) 類型的c函數

upvalue是建立C閉包時壓入的upvalue, 類型是TValue, 能夠得知, upvalue能夠是任意的lua類型

Lua閉包結構:

struct LClosure{  
           ClosureHeader;  
           strcut Proto* p;  
           UpVal* upvals[1];  
       }

Proto的結構比較複雜, 這裏先不作分析

統一的閉包結構, 一個聯合體, 說明一個閉包要麼是C閉包, 要麼是lua閉包, 這個是用isC表識出來的.

  union Closure{  
            CClosure c;  
            LClosure  l;  
       }

糾結的閉包

爲何你們叫閉包, 不叫它函數:

1. c 語言中的函數的定義: 對功能的抽象塊;

2. lua對函數作了擴展:

a. 能夠把幾個值和函數綁定在一塊兒, 這些值被稱爲upvalue.

ps: 可能有人以爲c++的函數對象也能夠把幾個值和函數綁定起來啊, 是這樣的, 可是這個問題就像是"在彙編中也能夠實現面向對象呀"同樣, lua從語言層面對upvalue提供了支持, 就像c++/java從語言層面提供了對類, 對象的支持同樣, 固然大大的解放了咱們程序員的工做量, 並且配上lua動態類型, 更是讓人輕鬆了很多.

b. 每一個函數能夠和一個env(環境)綁定.

ps: 若是說上面的upvalue還能在c++中coding出來, 那麼env 上下文環境這種動態語言中特有的東西c++就沒有明顯的對應結構了吧? 可能有人以爲lua是c寫的, 經過coding也能夠實現, 好吧=.= , "能作和作"是兩碼事, 就想你能步行從北京到上海, 不代表你就必需要這麼作. env是很是重要和有用的東西, 它能夠輕鬆創造出一個受限的環境, 就是傳說中的"沙盒", 我說的更通俗一點就是"一個動態名字空間機制". 這個先暫時不分析.

好了, 如今咱們看到

  c 函數 { 功能抽象 }

  lua 閉包 {功能抽象, upvalue, env}

重點: 閉包 == {功能抽象, upvalue, env}

  看到這裏, 你們都明白了, 若是把lua中的{功能抽象, upvalue, env}也稱爲函數, 不但容易引發你們的誤解覺得它就是和c函數同樣, 並且它確實不能很好的表達出lua函數的豐富內涵, 閉包, "閉" 是指的它是一個object, 一個看得見摸得着的東西, 不可分割的總體(first class); "包" 指的是它包含了功能抽象, upvalue, env. 這裏一個頗有趣的事實就是, {功能抽象, upvalue, env}是不少動態語言的一個實現特徵, 好比lua, javascript都有實現這樣的結構, 它是先被實現出來, 而後冠以"閉包"這樣一個名稱. 因此, 你單單想去理解閉包這個詞的話, 基本是沒有辦法理解的, 去網上查閉包, 沒用, 你能查到的

相關文章
相關標籤/搜索