第一版是为 Lua 5.0 编写的。虽然在很大程度上仍然适用于更高版本,但仍存在一些差异。
第四版针对 Lua 5.3,可在 亚马逊 和其他书店购买。
购买本书,您还可以帮助支持 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,该参数是封闭函数 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 是按钮按下时要调用的回调函数。(它实际上是一个闭包,因为它访问了 upvalue 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
现在,我们将旧版本保存在一个私有变量中;访问它的唯一方法是通过新版本。

您可以使用相同的特性来创建安全环境,也称为沙盒。在运行不受信任的代码(例如服务器通过 Internet 接收的代码)时,安全环境至关重要。例如,为了限制程序可以访问的文件,我们可以使用闭包重新定义 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 不会提供一刀切的解决方案,而是为您提供元机制,以便您可以根据您的特定安全需求定制您的环境。