当编写一个被其他函数 B
包含的函数 A
时,被包含的函数 A
可以访问包含其的函数 B
的所有局部变量,我们将这种特性称为 词法定界
。虽然这种可见性规则听上去很明确,但实际上并非如此。词法定界外加嵌套的第一类值函数可以为编程语言提供强大的功能,但很多编程语言并不支持将这两者组合使用。
先看一个简单的例子,假设有一个表,其中包含了学生的姓名和对应的成绩,如果我们想基于分数对学生姓名排序,分数高者在前,那么可以使用如下的代码完成上述需求:
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
既不是全局变量也不是局部变量,而是我们所述的非局部变量。
提示
由于历史原因,在 Lua
语言中非局部变量也被称为上值。
这一点之所以如此有趣是因为,函数作为第一类值,能够逃逸出他们变量的原始定界范围。考虑如下代码:
function newCounter() local count = 0 return function () count = count + 1 return count end end c1 = newCounter() print(c1()) --> 1 print(c1()) --> 2
在上述代码中,匿名函数访问了一个非全局变量( count
)并将其当做计数器。然而,由于创建变量的函数( newCounter
)已经返回,因此当我们调用匿名函数时,变量 count
似乎已经超出了作用范围。但其实不然,由于闭包概念的存在, Lua
语言能够正确地应对这种情况。简单地说,一个闭包就是一个函数外加能够使该函数正确访问非局部变量所需的其他机制。如果我们再次调用 newCounter
,那么一个新的局部变量 count
和一个新的闭包会被创建出来,这个新的闭包针对的是这个新变量:
c2 = newCounter() print(c2()) --> 1 print(c1()) --> 3 print(c2()) --> 2
因此, c1
和 c2
是不同的闭包。他们建立在相同的函数之上,但是各自拥有局部变量 count
的独立实例。
从技术上将, Lua
语言中只有闭包而没有函数。函数本身只是闭包的一种原型。不过尽管如此,只要不会引起混淆,我们就仍将使用术语“函数”来代指闭包。
闭包在许多场合中均是一种有价值的工具。正如我们之前已经见到过的,闭包在作为诸如 sort
这样的高阶函数的参数时就非常有用。同样,闭包对于创建了其他函数的函数也很有用,例如我们之前的 newCounter
实例及求导数的示例。这种机制使得 Lua
程序能够综合运用函数式编程世界中多种精妙的编程技巧。另外,闭包对于回调函数来说也很有用。对于回调函数而言,一个典型的例子就是在传统 GUI
工具箱中创建按钮。每个按钮通常都对应一个回调函数,当用户按下按钮时,完成不同的处理动作的回调函数就会被调用。
例如,假设有一个具有 10
个类似按钮的数字计算器(每个按钮代表一个十进制数字),我们就可以使用如下的函数来创建这些按钮:
function digitButton(digit) return Button{ label = tostring(digit), action = function () add_to_display(digit) end } end
在上述示例中,假设 Button
是一个创建新按钮的工具箱函数, label
是按钮的标签, action
是当按钮按下时被调用的回调函数。回调可能发生在函数 digitButton
早已执行完后,那是变量 digit
已经超出了作用范围,但闭包仍可以访问它。
闭包在另一种很不一样的场景下也非常有用。由于函数可以被保存在普通变量中,因此在 Lua
语言中可以轻松地重新定义函数,甚至预定义函数。这种机制也正是 Lua
语言灵活的原因之一。通常,当重新定义一个函数的时候,我们需要在新的实现中调用原来的那个函数。例如,假设要重新定义函数 sin
以使其参数以角度为单位而不是以弧度为单位。那么这个新函数就可以先对参数进行转换,然后再调用原来的 sin
函数进行真正的计算。代码可能形如:
local 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
上述代码使用了 do
代码段来限制局部变量 oldSin
的作用范围;根据可见性规则,局部变量 oldSin
只在这部分代码段中有效。因此,只有新版本的函数 sin
才能访问原来的 sin
函数,其他部分的代码则访问不了。
我们可以使用同样的技巧来创建安全的运行时环境,即所谓的沙盒( sandbox
)。当执行一些诸如从远程服务器上下载到的未受信任代码时,安全的运行时环境非常重要。例如,我们可以通过使用闭包重新定义函数 io.open
来限制一个程序能够访问的文件:
do local oldOpen = io.open local access_OK = function (filename, mode) check access end io.open = function (filename, mode) if access_OK(filename, mode) then return oldOpen(filename, mode) else return nil, "access denied" end end end
上述实例的巧妙之处在于,经过重新定以后,一个程序就只能通过新的受限版本来调用原来未受限版本的 io.open
函数。示例代码将原来不安全的版本保存为闭包的一个私有变量,该变量无法从外部访问。通过这一技巧,就可以在保证简洁性和灵活性的前提下在 Lua
语言本身上构建 Lua
沙盒。相对于提供一套大而全的解决方案, Lua
语言提供的是一套 元机制
。接着这种机制可以根据特定的安全需求来裁剪具体的运行时环境。
Tip
真实的沙盒除了保护外部文件外还有更多的功能。