Lua中的closure(閉合函數)


詞法域:若將一個函數寫在另一個函數之內,那么這個位於內部的函數便可以訪問外部函數中的局部變量,這項特征稱之為“詞法域”。

例:假設有一個學生姓名的列表和一個對應於沒個姓名的年級列表,需要根據每個學生的年級來對他們的姓名進行排序(由高到低)。可以這么做:

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)”,因此可以根據特定的安全需要來創建一個安全的運行環境。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM