Lua的function、closure和upvalue(轉)

post0發表於2007-08-12
Lua的function、closure和upvalue(轉)[@more@]

  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很方便,但它們的語義也很微妙,需要引起注意。比如將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妥善儲存好了,之後f1、f2如果在當前堆疊上找不到n就會自動到它們的外包閉包(姑且這麼叫)的upvalue引用陣列中去找,並把找到的引用值複製到自己的upvalue引用陣列中。仔細觀察上述程式碼,可以判定g1和g2與f1和f2共享同一個upvalue。這是為什麼呢?其實,g1和g2與f1和f2都是同一個閉包(t)建立的,所以它們引用的upvalue(n)實際也是同一個變數,而剛才描述的搜尋機制則保證了最後它們的upvalue引用都會指向同一個地方。

  

  Lua將函式做為基本型別值並支援詞法定界的特性使得語言具有強大的抽象能力。而透徹認識函式、閉包和upvalue將幫助程式設計師善用這種能力

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-951584/,如需轉載,請註明出處,否則將追究法律責任。

相關文章