闭包的Lua中
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征在lua中我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;想根据学生的成绩从高到低对学生进行排序, 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) 例子中包含在sortbygrade函数内部的sort中的匿名函数可以访问sortbygrade的参数grades,在匿名函数内部grades不是全局变量也不是局部变量,我们称作外部的局部变量(external local variable)或者upvalue。(upvalue意思有些误导,然而在Lua中他的存在有历史的根源,还有他比起external local variable简短)。
看下面的代码:
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已经超出了作用范围,因为创建i的函数newCounter已经返回了。然而Lua用闭包的思想正确处理了这种情况。简单的说,闭包是一个函数以及它的upvalues。如果我们再次调用newCounter,将创建一个新的局部变量i,因此我们得到了一个作用在新的变量i上的新闭包。
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
c1、c2是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代指闭包。
闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在Lua的函数世界里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在GUI环境中你需要创建一系列button,但用户按下button时回调函数被调用,可能不同的按钮被按下时需要处理的任务有点区别。具体来讲,一个十进制计算器需要10个相似的按钮,每个按钮对应一个数字,可以使用下面的函数创建他们:
function digitButton (digit)
return Button{ label = digit,
action = function ()
add_to_display(digit)
end
}
end
这个例子中我们假定Button是一个用来创建新按钮的工具,label是按钮的标签,action是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问upvalue digit)。digitButton完成任务返回后,局部变量digit超出范围,回调函数仍然可以被调用并且可以访问局部变量digit。
闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义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的唯一方式是通过新版本的函数。
利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java里的沙箱一样),当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io库的open函数来限制程序打开的文件。
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 Scheme中的闭包
其他编程的语言主要采用的是闭包的第二种意义(一个与闭包毫不相干的概念):闭包也算一种为表示带有自由变量的过程而用的实现技术。但Scheme的术语“闭包”来自抽象代数。在抽象代数里,一集元素称为在某个运算(操作)之下封闭,如果将该运算应用于这一集合中的元素,产生出的仍然是该集合里的元素。
用Scheme的序对举例,为了实现数据抽象,Scheme提供了一种称为序对的复合结构。这种结构可以通过基本过程cons构造出来。过程cons取两个参数,返回一个包含这两个参数作为其成分的复合数据对象。请注意,一个序对也算一个数据对象。进一步说,还可以用cons去构造那种其元素本身就是序对的序对,并继续这样做下去。
(define x (cons 1 2)) //构造一个x序对,有1,2组成
(define y (cons 3 4))
(define z (cons x y))
Scheme可以建立元素本身也算序对的序对,这就是表结构得以作为一种表示工具的根本基础。我们将这种能力称为cons的闭包性质。一般说,某种组合数据对象的操作满足闭包性质,那就是说,通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合。闭包性质是任何一种组合功能的威力的关键要素,因为它使我们能够建立起层次性结构,这种结构由一些部分构成,而其中的各个部分又是由它们的部分构成,并且可以如此继续下去。