Lua 5.0實現 4 函數和閉包

當Lua編譯一個函數時,會生成一個原型(prototype),該原型包含了函數的虛擬機指令,常量(數字,字符串等),和一些調試信息。在運行期,任何時候Lua執行一個function...end表達式,都會創建一個新的閉包(closure)。每一個閉包有一個與之相應的原型的引用,一個環境的引用(一個保存全局變量的表),以及一個包含了upvalue的引用的數組,通過該數組可以訪問外層函數的局部變量。

作用域(即變量的生存期)以及first-class函數給如何在內嵌函數中訪問外部函數的局部變量出了一個衆所周知的難題。考慮圖3的例子。當add2被調用時,會訪問外部函數的局部變量x(在Lua中函數的參數被當做局部變量處理)。但是,當add2被調用時,創建add2的函數add已經返回。如果x是在棧中被創建的,棧中存放x的地方已經被銷燬。

first-class函數:函數Lua裏面被當做變量處理,也就是說可以在函數裏面定義函數。

clip_image002

許多過程語言,通過限制作用域(如Python),或者不提供first-class函數(如Pascal),或者兩者都做(如C)來避免這個難題。函數式語言沒有這個限制。像Scheme和ML這樣的非純函數語言已經在如何編譯閉包的技術上有了廣泛的研究。然而他們都沒有考慮限制編譯器的複雜度。例如,Scheme編譯器的優化器Bigloo的控制流分析模塊是Lua實現的10倍:Bigloo 2.6f的控制流分析有106350行,vs. Lua5.0的核心有10155行。正如第二節說的那樣,Lua需要一種更簡單的實現。

Lua使用一種叫upvalue的結構來實現閉包。任何對外層局部變量的訪問都間接地通過訪問upvalue完成。upvalue一開始指向棧中變量存儲的地方(圖4,左)。當變量離開作用域的時候,upvalue將變量遷移到upvalue的槽中(圖4,右)。(淺拷貝)因爲upvalue的值是通過一個指針來訪問的,所以這種遷移對於訪問upvalue中變量的代碼來說是透明的。與內嵌函數不同,聲明變量的函數通過棧來訪問該變量。

clip_image004

爲了保證閉包之間在頻繁改變的棧中能夠正確地共享外層變量,每一個變量至多創建一個upvalue並在其他閉包需要時重用該upvalue。爲了保證這種唯一性,Lua用一個鏈表(圖4中的pending vars list)來保存棧中的所有open upvalue(就是仍然指向棧中數據的upvalue)。當Lua創建一個閉包的時候,會掃描所有外層局部變量。對於每個變量,如果相應的upvalue已經在open upvalue鏈表中,就複用它。否則,Lua會爲這個變量創建一個新的upvalue並保存在open upvalue鏈表中。注意在open upvalue鏈表中查找對應的upvalue通常會在探測少數節點後就結束,因爲對於每個會被內嵌函數用到的局部變量,鏈表最多隻包含一個訪問入口。一旦一個close upvalue沒有被任何閉包使用,最終會被垃圾回收。

外層函數返回的時候,open upvalue鏈表爲空

一個函數有可能訪問更外層的局部變量。這種情況下,甚至創建閉包的時候,變量就可能不在棧中了。Lua通過使用flat閉包來解決這個問題。有了flat閉包,當一個函數訪問一個不在外一層函數的棧中的外層局部變量時,就會去外一層的閉包中查找。這樣,當一個函數被創建的時候,閉包中的所有變量,不是在外一層函數的棧中,就在外一層函數的閉包中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章