闭包概念在C++,.Net这些语言到是用的少,而在脚本语言比如javascript、perl、lua比较常见
下面我就讲一下lua脚本语言下的闭包,最好动动手才能有更深的印象
如下是lua语句
输出结果如下
function f1()
i=0
local function f2()
i=i+1
return i
end
i=i+1
return f2
end
函数的闭包实际上是一个函数嵌套在另一个函数中,当然还不止只这些,可以看到
g1 = f1()
print(g1())
print(g1())
输出的是2,3
这也就是闭包的一个特性,局部变量存在于堆栈之中,并不被释放掉
lua语言有如下两个特点:
1.lua中的函数可以被存放在变量、表中,也可以作为函数的参数,还可以作为函数的返回值,如:
func = function()
print("Hello");
end
等价于
function func()
print("Hello");
end
这两种写法都是函数func()的定义;并且下面这个例子:
function func()
return function() print("Hello"); end --函数作为返回值
end
local f1 = func();
print(f1); --运行结果:function:00379738
f1(); --运行结果:Hello
是函数作为函数返回值的情况,其中f1是func()内部返回的函数,打印出来为函数在内存中的地址,f1()调用了内部函数,执行后打印出了"Hello"结果。
2.被嵌套的函数可以访问他外部函数中的变量(这个比较强悍,C语言只能访问全局的或以参数形式传入)。
看如下例子:
function func()
local index = 0;
local inner = function()
print(index);
index = index + 1;
end
inner(); --打印结果:0
inner(); --打印结果:1
print(index); --打印结果:2
end
func();
说明了func()中的局部变量index是可以在inner()中被使用和更改的,index被称为inner()的upvalue。
以上这两点是构成闭包的基础,也是lua语言的强大和灵活之处。
另外,将上面例子稍加改造,如下:
function func()
local index = 0;
print("Hello");
return function()
print(index);
index = index + 1;
end
end
local inner = func(); --调用func()函数,返回内部函数给inner,打印结果:"Hello"
print(inner); --打印结果:function:0037BE88
inner(); --调用内部函数,打印结果:0
inner(); --调用内部函数,打印结果:1
local other = func(); --获取另一个内部函数实例,打印结果:"Hello"
other(); --调用另一个内部函数实例,打印结果:0
other(); --同上,打印结果:1
由此可以看出函数的局部变量是可以保存在函数内部的,通过调用该函数内嵌的函数可以获取并修改局部变量的值,该函数的局部变量(upvalue)和内嵌函数的组合使用,形成了闭包,这看起来与C++中类的成员变量有些类似。函数具有闭包的功能后,不必再担心其局部变量没办法保存了,这一点对程序员的影响是很大的。
还有如下例子:
function func();
local index = 0;
return function()
index = index + 1;
return index;
end
end
local inner = func();
print(inner()); --打印结果:1
print(inner()); --打印结果:2
调用了func()的内部函数实例,并将函数调用结果打印了出来,是个很好的保存局部变量的方式。
另外闭包常用于泛型for的迭代器中,由于太晚了,就不再赘述了。有问题欢迎讨论拍砖。
Lua中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值相同(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然能定义函数。假设函数f2定义在函数f1中,那么就称f2为f1的内嵌(inner)函数,f1为f2的外包(enclosing)函数,外包和内嵌都具有传递性,即f2的内嵌必然是f1的内嵌,而f1的外包也一定是f2的外包。内嵌函数能访问外包函数已创建的所有局部变量,这种特性便是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或upvalue。试看如下代码:
function f1(n)
-- 函数参数也是局部变量
local function f2()
print(n) -- 引用外包函数的局部变量
end
return f2
end
g1 = f1(1979)
g1() -- 打印出1979
g2 = f1(500)
g2() -- 打印出500
当执行完g1 = f1(1979)后,局部变量n的生命本该结束,但因为他已成了内嵌函数f2(他又被赋给了变量g1)的upvalue,所以他仍然能以某种形式继续“存活”下来,从而令g1()打印出正确的值。
可为什么g2和g1的函数体相同(都是f1的内嵌函数f2的函数体),但打印值不同?这就涉及到一个相当重要的概念——闭包(closure)。事实上,Lua编译一个函数时,会为他生成一个原型(prototype),其中包含了函数体对应的虚拟机指令、函数用到的常量值(数,文本字符串等等)和一些调试信息。在运行时,每当Lua执行一个形如function...end 这样的表达式时,他就会创建一个新的数据对象,其中包含了相应函数原型的引用、环境(environment,用来查找全局变量的表)的引用及一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。g1和g2的值严格来说不是函数而是闭包,并且是两个不相同的闭包,而每个闭包能保有自己的upvalue值,所以g1和g2打印出的结果当然就不相同了。
使用upvalue非常方便,但他们的语义也非常微妙,需要引起注意。比如将f1函数改成:
function f1(n)
local function f2()
print(n)
end
n = n + 10
return f2
end
g1 = f1(1979)
g1() -- 打印出1989
内嵌函数定义在n = n + 10这条语句之前,可为什么g1()打印出的却是1989?upvalue实际是局部变量,而局部变量是保存在函数堆栈框架上(stack frame)的,所以只要upvalue还没有离开自己的作用域,他就一直生存在函数堆栈上。这种情况下,闭包将通过指向堆栈上的upvalue的引用来访问他们,一旦upvalue即将离开自己的作用域(这也意味着他马上要从堆栈中消失),闭包就会为他分配空间并保存当前的值,以后便可通过指向新分配空间的引用来访问该upvalue。当执行到f1(1979)的n = n + 10时,闭包已创建了,不过n并没有离开作用域,所以闭包仍然引用堆栈上的n,当return f2完成时,n即将结束生命,此时闭包便将n(已是1989了)复制到自己管理的空间中以便将来访问。弄清晰了内部的秘密后,运行结果就不难解释了。
upvalue还能为闭包之间提供一种数据共享的机制。试看下例:
function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end
f1,f2 = Create(1979)
f1() -- 打印1979
f2()
f1() -- 打印1989
f2()
f1() -- 打印1999
f1,f2这两个闭包的原型分别是Create中的内嵌函数foo1和foo2,而foo1和foo2引用的upvalue是同一个,即Create的局部变量n。前面已说过,执行完Create调用后,闭包会把堆栈上n的值复制出来,那么是否f1和f2就分别拥有一个n的拷贝呢?其实不然,当Lua发现两个闭包的upvalue指向的是当前堆栈上的相同变量时,会聪明地只生成一个拷贝,然后让这两个闭包共享该拷贝,这样任一个闭包对该upvalue进行修改都会被另一个探知。上述例子非常清晰地说明了这点:每次调用f2都将upvalue的值增加了10,随后f1将更新后的值打印出来。upvalue的这种语义非常有价值,他使得闭包之间能不依赖全局变量进行通讯,从而使代码的可靠性大大提高。
闭包在创建之时其upvalue就已不在堆栈上的情况也有可能发生,这是因为内嵌函数能引用更外层外包函数的局部变量:
function Test(n)
local function foo()
local function inner1()
print(n)
end
local function inner2()
n = n + 10
end
return inner1,inner2
end
return foo
end
t = Test(1979)
f1,f2 = t()
f1() -- 打印1979
f2()
f1() -- 打印1989
g1,g2 = t()
g1() -- 打印1989
g2()
g1() -- 打印1999
f1() -- 打印1999
执行完t = Test(1979)后,Test的局部变量n就“死”了,所以当f1,f2这两个闭包被创建时堆栈上根本未找到n的踪影,这叫他们怎么取得n的值呢?呵呵,不要忘了Test函数的n不仅仅是inner1和inner2的upvalue,同时他也是foo的upvalue。t = Test(1979)之后,t这个闭包一定已把n妥善保存好了,之后f1、f2如果在当前堆栈上未找到n就会自动到他们的外包闭包(姑且这么叫)的upvalue引用数组中去找,并把找到的引用值拷贝到自己的upvalue引用数组中。仔细观察上述代码,能判定g1和g2和f1和f2共享同一个upvalue。这是为什么呢?其实,g1和g2和f1和f2都是同一个闭包(t)创建的,所以他们引用的upvalue(n)实际也是同一个变量,而刚才描述的搜索机制则确保了最后他们的upvalue引用都会指向同一个地方。
Lua将函数做为基本类型值并支持词法定界的特性使得语言具有强大的抽象能力。而透彻认识函数、闭包和upvalue将帮助程式员善用这种能力。