第一版是針對 Lua 5.0 編寫的。雖然在很大程度上仍然適用於後續版本,但仍有些差異。
第四版針對 Lua 5.3,可在 Amazon 和其他書店購買。
購買這本書,您也可以幫助 支援 Lua 專案


6.1 – 閉包

當一個函式寫在另一個函式內時,它可以完全存取封閉函式的局部變數;這個功能稱為詞彙範圍。雖然這聽起來很明顯,但事實並非如此。詞彙範圍加上一級函式是程式語言中強大的概念,但很少有語言支援這個概念。

讓我們從一個簡單的範例開始。假設您有一個學生姓名清單和一個將姓名與成績關聯的表格;您想要根據成績(較高成績在前)對姓名清單進行排序。您可以這樣執行這項工作

    names = {"Peter", "Paul", "Mary"}
    grades = {Mary = 10, Paul = 7, Peter = 8}
    table.sort(names, function (n1, n2)
      return grades[n1] > grades[n2]    -- compare the grades
    end)
現在,假設您想要建立一個函式來執行這項工作
    function sortbygrade (names, grades)
      table.sort(names, function (n1, n2)
        return grades[n1] > grades[n2]    -- compare the grades
      end)
    end
範例中有趣的地方在於傳遞給 sort 的匿名函式存取參數 grades,而 grades 是封閉函式 sortbygrade 的局部變數。在這個匿名函式內,grades 不是全域變數也不是局部變數。我們稱它為外部局部變數upvalue。(「upvalue」這個術語有點誤導,因為 grades 是變數,而不是值。然而,這個術語在 Lua 中有歷史淵源,而且比「外部局部變數」簡潔。)

為什麼這很有趣?因為函式是一級值。考慮以下程式碼

    function newCounter ()
      local i = 0
      return function ()   -- anonymous function
               i = i + 1
               return i
             end
    end
    
    c1 = newCounter()
    print(c1())  --> 1
    print(c1())  --> 2
現在,匿名函式使用一個 upvalue i 來維持它的計數器。然而,當我們呼叫匿名函式時,i 已經超出範圍,因為建立那個變數的函式 (newCounter) 已經傳回。儘管如此,Lua 透過使用閉包的概念正確地處理那個情況。簡單來說,閉包是一個函式加上它正確存取其 upvalue 所需的一切。如果我們再次呼叫 newCounter,它將建立一個新的局部變數 i,因此我們將取得一個新的閉包,作用於那個新變數
    c2 = newCounter()
    print(c2())  --> 1
    print(c1())  --> 3
    print(c2())  --> 2
因此,c1c2 是同一個函式的不同封閉,且每個函式作用於區域變數 i 的獨立實例。技術上來說,Lua 中的值是封閉,而不是函式。函式本身只是封閉的原型。不過,只要沒有混淆的可能,我們仍會繼續使用「函式」一詞來指稱封閉。

封閉在許多情況下提供有價值的工具。正如我們所見,它們可用作高階函式的引數,例如 sort。封閉對於建立其他函式的函式也很有價值,例如我們的 newCounter 範例;此機制允許 Lua 程式納入函式世界中的精緻程式設計技巧。封閉也可用於 回呼 函式。典型的範例發生在您在典型的 GUI 工具組中建立按鈕時。每個按鈕都有回呼函式,當使用者按下按鈕時會呼叫該函式;您希望不同的按鈕在按下時執行稍微不同的動作。例如,數位計算器需要十個類似的按鈕,每個按鈕代表一個數字。您可以使用類似下一個函式建立每個按鈕

    function digitButton (digit)
      return Button{ label = digit,
                     action = function ()
                                add_to_display(digit)
                              end
                   }
    end
在此範例中,我們假設 Button 是建立新按鈕的工具組函式;label 是按鈕標籤;action 是按下按鈕時要呼叫的回呼函式。(它實際上是一個封閉,因為它會存取上值 digit。)回呼函式可以在 digitButton 完成其任務並且區域變數 digit 超出範圍很長一段時間後呼叫,但它仍然可以存取該變數。

閉包在完全不同的情況下也很有價值。由於函式儲存在一般變數中,我們可以輕鬆地在 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
現在,我們將舊版本保存在私人變數中;唯一可以存取它的方法是透過新版本。

您可以使用相同的特點來建立安全環境,也稱為「沙盒」。當執行不受信任的程式碼(例如伺服器透過網際網路接收的程式碼)時,安全環境至關重要。例如,若要限制程式可以存取的檔案,我們可以使用閉包重新定義 open 函式(來自 io 函式庫)

    do
      local oldOpen = io.open
      io.open = function (filename, mode)
        if access_OK(filename, mode) then
          return oldOpen(filename, mode)
        else
          return nil, "access denied"
        end
      end
    end
此範例的優點在於,在重新定義之後,程式無法呼叫不受限制的 open,只能透過新的受限制版本呼叫。它將不安全的版本作為閉包中的私人變數,無法從外部存取。有了此功能,您可以使用 Lua 本身在 Lua 中建構沙盒,並享有通常的優點:靈活性。Lua 沒有提供一體適用的解決方案,而是提供一個元機制,讓您可以根據特定安全性需求調整您的環境。