当一个函数被写入另一个函数时,它可以完全访问函数中的局部变量; 这个特征被称为词法范围。虽然这听起来很明显,但事实并非如此。在编程语言中,词法范围界定和一流功能是一个强大的概念,但很少有语言支持该概念。
让我们从一个简单的例子开始。假设你有一个学生姓名列表和一个将姓名与成绩联系起来的表格; 你想根据他们的成绩(首先是更高的成绩)对名单进行排序。您可以按如下方式完成此任务:
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)
//Now, suppose you want to create a function to do this task:
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使用闭包的概念正确地处理了这种情况。简而言之,闭包是一个函数,加上所有需要正确访问它的upvalues。如果我们newCounter再次调用它,它将创建一个新的局部变量i,所以我们将得到一个新的闭包,对这个新变量进行操作:
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
所以,c1和c2是在同一功能不同封口并且在局部变量的独立实例化每个行为 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为您提供了一种元机制,而不是一个通用的解决方案,因此您可以根据特定的安全需求量身定制您的环境。