詞法域:若將一個函數寫在另一個函數之內,那么這個位於內部的函數便可以訪問外部函數中的局部變量,這項特征稱之為“詞法域”。
例:假設有一個學生姓名的列表和一個對應於沒個姓名的年級列表,需要根據每個學生的年級來對他們的姓名進行排序(由高到低)。可以這么做:
names = {"Peter", "Paul", "Mary"} grades = {Mary = 10, Paul = 7, Peter = 8} table.sort(names, function (n1, n2) return grades[n1] > grades[n2] -- 比較年級 end)
現在假設單獨創建一個函數來做這項工作:
function sortbygrade (names, grades) table.sort(names, function (n1, n2) return grades[n1] > grades[n2] -- 比較年級 end) end
上例中有一點很有趣,傳遞給sort的匿名函數可以訪問參數grades,而grades是外部函數sortbygrade的局部變量。在這個匿名函數內部,grades既不是全局變量也不是局部變量,將其稱為一個“非局部的變量(non-local variable)”。
為什么在Lua中允許這種訪問呢?運因在與函數是“第一類值”。考慮一下代碼:
function newCounter() local i = 0 return function () -- 匿名函數 i = i + 1 return i end end c1 = newCounter() print(c1()) --> 1 print(c1()) --> 2
在這段代碼中,匿名函數訪問了一個“非局部的變量”i,改變兩用於保持一個計數器。出刊上去,由於創建變量i的函數(newCounter)已經返回,所以之后每次調用匿名函數時,i都應該是已超出作用范圍的。但其實不然,Lua會以closure的概念來正確地處理這種情況。簡單地說,一個closure就是一個函數加上該函數所需訪問的所有“非局部的變量”。如果再次調用newCounter,那么它會創建一個新的局部變量i,從而也將得到一個新的closure:
c2 = newCOunter() print(c2()) --> 1 print(c1()) --> 3 print(c2()) --> 2
因此c1和c2是同一個函數所創建的兩個不同的closure,它們各自擁有局部變量i的獨立實例。
從技術上講,Lua中只有closure,而不存在“函數”。因為,函數本身就是一種特殊的closure。不過只要不會引起混淆,仍將采用屬於“函數”來指代closure。
在很多場合中closure都是一種很有價值的工具。就像只前所看到的,它們可作為sort這類高階函數的參數。closure對於那些創建其他函數的函數也很有價值,例如前例中的newCounter。這種機制使Lua程序可以混合那些在函數式百年成世界中久經考驗的編程技術。另外,closure對於回調函數也很有用。這里有一個典型的例子,假設有一個傳統的GUI工具包可以創建按鈕,每個按鈕都有一個回調函數,每當用戶按下按鈕時GUI工具包都會調用這些回調函數。再假設,基於此要做一個十進制計算器,其中需要10個數字按鈕。會發現這些按鈕之間的區別其實並不大,僅需在按下不同按鈕時做一些稍微不同的操作就可以了。那么可以使用以下函數來創建這些按鈕:
function digitButton (digit) return Button{ label = tostring(digit), action = function () add_to_display(digit) end } end
closure在另一種情況中也非常有用。例如在Lua中函數是存儲在普通變量中的,因此可以輕易地重新定義某些函數,甚至是重新定義那些預定以的函數。這正是Lua相當靈活的原因之一。通常當重新定義一個函數的時候,需要在新的視線中調用原來的那個函數。舉例來說,假設要重新定義函數sin,使其參數能使用角度來替換原先的弧度。那么這個心寒數就必須得轉換他的實參,並調用原來的sin函數完成真正的計算。這段代碼可能是這樣的:
oldSin = math.sin math.sin = function (x) return oldSin(x*math.pi/180) end
還有一種更徹底的做法是這樣的:
do local oldSin = math.sin local k = math.pi/180 math.sin = function (x) return oldSin(x*k) end end
將老版本的sin保存到了一個私有變量中,現在只有通過新版本的sin才能訪問它了。
可以使用同樣的技術來創建一個安全地運行環境,即所謂的“沙盒(sandbox)”。當執行一些未受信任的代碼時就需要一個安全地運行環境,例如在服務器中執行那些從Internet上接收到的代碼。舉例來說,如果要限制一個程序訪問文件的話,只需使用closure來重定義函數io.open就可以了。
do local oldOpen = io.open local access_OK = function (filename, mode) <檢查訪問權限> end io.open = function (filename, mode) if access_OK(filename, mode) then return oldOpen(filename, mode) else return nil, "access denied" end end end
這個示例的精彩之處在於,經過重新定義后,一個程序就只能呢該通過新的受限版本來調用原來哪個未受限的open函數了。示例將原來不安全的版本保存到closure的一個私有變量中,從而使得外部再也無法直接訪問到原來的版本了。通過這種技術,可以在Lua的語言層面上就構建除一個安全地運行環境,且不是簡易性了靈活性。相對於提供一套大而全的解決方案,Lua提供的則是一套“元機制(meta-mechanism)”,因此可以根據特定的安全需要來創建一個安全的運行環境。