lua的闭包是个新概念,理解它须要个过程。今天在网上找了几篇文章看,不错,先记录下。
1,lua闭包普通篇 http://hi.baidu.com/happynp/blog/item/b7736a1f7f65b3ffe0fe0b90.html
2,lua闭包文艺篇 http://www.ibm.com/developerworks/cn/linux/l-cn-closure/
3,lua闭包2B篇 http://www.cnblogs.com/ringofthec/archive/2010/11/05/luaClosure.html
———————————————————————————————————————————————javascript
lua中的函数是一阶类型值(first-class value),定义函数就象建立普通类型值相同(只不过函数类型值的数据主要是一条条指令而已),因此在函数体中仍然能定义函数。假设函数f2定义在函数f1中,那么就称f2为f1的内嵌(inner)函数,f1为f2的外包(enclosing)函数,外包和内嵌都具备传递性,即f2的内嵌必然是f1的内嵌,而f1的外包也必定是f2的外包。内嵌函数能访问外包函数已建立的全部局部变量,这种特性即是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或upvalue。试看以下代码:html
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函数改为:java
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还能为闭包之间提供一种数据共享的机制。试看下例:linux
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就已不在堆栈上的状况也有可能发生,这是由于内嵌函数能引用更外层外包函数的局部变量:c++
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妥善保存好了,以后f一、f2若是在当前堆栈上未找到n就会自动到他们的外包闭包(姑且这么叫)的upvalue引用数组中去找,并把找到的引用值拷贝到本身的upvalue引用数组中。仔细观察上述代码,能断定g1和g2和f1和f2共享同一个upvalue。这是为何呢?其实,g1和g2和f1和f2都是同一个闭包(t)建立的,因此他们引用的upvalue(n)实际也是同一个变量,而刚才描述的搜索机制则确保了最后他们的upvalue引用都会指向同一个地方。
Lua将函数作为基本类型值并支持词法定界的特性使得语言具备强大的抽象能力。而透彻认识函数、闭包和upvalue将帮助程式员善用这种能力。程序员
—————————————————————————————————————————————————————————
什么是闭包?
闭包并非什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定义有不少种说法,这些说法大致能够分为两类:
一种说法认为闭包是符合必定条件的函数,好比参考资源中这样定义闭包:闭包是在其词法上下文中引用了自由变量的函数。
另外一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。好比参考资源中就有这样的的定义:在实现深约束时,须要建立一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一块儿,这样捆绑起来的总体被称为闭包。
这两种定义在某种意义上是对立的,一个认为闭包是函数,另外一个认为闭包是函数和引用环境组成的总体。虽然有些咬文嚼字,但能够确定第二种说法更确切。闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就肯定了,不会在执行时发生变化,因此一个函数只有一个实例。闭包在运行时能够有多个实例,不一样的引用环境和相同的函数组合能够产生不一样的实例。所谓引用环境是指在程序执行中的某个点全部处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所表明的对象之间的联系。那么为何要把引用环境与函数组合起来呢?这主要是由于在支持嵌套做用域的语言中,有时不能简单直接地肯定函数的引用环境。这样的语言通常具备这样的特性:
函数是一阶值(First-class value),即函数能够做为另外一个函数的返回值或参数,还能够做为一个变量的值。
函数能够嵌套定义,即在一个函数内部能够定义另外一个函数。
这些概念上的解释很难理解,显然一个实际的例子更能说明问题。Lua 语言的语法比较接近伪代码,咱们来看一段 Lua 的代码:
清单 1. 闭包示例1 编程
在这段程序中,函数 inc_count 定义在函数 make_counter 内部,并做为 make_counter 的返回值。变量 count 不是 inc_count 内的局部变量,按照最内嵌套做用域的规则,inc_count 中的 count 引用的是外层函数中的局部变量 count。接下来的代码中两次调用 make_counter() ,并把返回值分别赋值给 c1 和 c2 ,而后又依次打印调用 c1 和 c2 所获得的返回值。
这里存在一个问题,当调用 make_counter 时,在其执行上下文中生成了局部变量 count 的实例,因此函数 inc_count 中的 count 引用的就是这个实例。可是 inc_count 并无在此时被执行,而是做为返回值返回。当 make_counter 返回后,其执行上下文将失效,count 实例的生命周期也就结束了,在后面对 c1 和 c2 调用实际是对 inc_count 的调用,而此处并不在 count 的做用域中,这看起来是没法正确执行的。
上面的例子说明了把函数做为返回值时须要面对的问题。当把函数做为参数时,也存在类似的问题。下面的例子演示了把函数做为参数的状况。
清单 2. 闭包示例2 数组
function do5times(func) local total = 0 for i=0, 5 do total = func(i) end return total end function aa() local sum = 0 function addsum(i) sum = sum + i return sum end return addsum end s = aa() s1 = aa() print(do5times(s)) print(do5times(s1))
这里咱们看到,函数 addsum 被传递给函数 do5times,被并在 do5times 中被调用5次。不难看出 addsum 实际的执行点在 do5times 内部,它要访问非局部变量 sum,而 do5times 并不在 sum 的做用域内。这看起来也是没法正常执行的。
这两种状况所面临的问题实质是相同的。在这样的语言中,若是按照做用域规则在执行时肯定一个函数的引用环境,那么这个引用环境可能和函数定义时不一样。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个总体。当把这个总体看成函数调用时,先把其中的引用环境覆盖到当前的引用环境上,而后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由引用环境与函数代码组成的实体就是闭包。固然若是编译器或解释器可以肯定一个函数在定义和运行时的引用环境是相同的,那就没有必要把引用环境和代码组合起来了,这时只须要传递普通的函数就能够了。如今能够得出这样的结论:闭包不是函数,只是行为和函数类似,不是全部被传递的函数都须要转化为闭包,只有引用环境可能发生变化的函数才须要这样作。
再次观察上面两个例子会发现,代码中并无经过名字来调用函数 inc_count 和 addsum,因此他们根本不须要名字。以第一段代码为例,它能够重写成下面这样:
清单 3. 闭包示例3 闭包
function make_counter() local count = 0 return function() count = count + 1 return count end end c1 = make_counter() c2 = make_counter() print(c1()) print(c2())
这里使用了匿名函数。使用匿名函数能使代码获得简化,同时咱们也没必要挖空心思地去给一个不须要名字的函数取名字了。
上面简单地介绍了闭包的原理,更多的闭包相关的概念和理论请参考参考资源中的"名字,做用域和约束"一章。
一个编程语言须要哪些特性来支持闭包呢,下面列出一些比较重要的条件:
1. 函数是一阶值;
2. 函数能够嵌套定义;
3. 能够捕获引用环境,并把引用环境和函数代码组成一个可调用的实体;
4. 容许定义匿名函数;
这些条件并非必要的,但具有这些条件能说明一个编程语言对闭包的支持较为完善。另外须要注意,有些语言使用与函数定义不一样的语法来定义这种能被传递的"函数",如 Ruby 中的 Block。这其实是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。
借用一个很是好的说法来作个总结:对象是附有行为的数据,而闭包是附有数据的行为。
——————————————————————————————————————————————————————app
lua中有两种闭包, c闭包和lua闭包
两种闭包的公共部分:
#define ClosureHeader CommonHeader; lu_byte isC; /*是不是C闭包*/ lua_byte nupvalues; /*upval的个数*/ GCObject* gclist; struct Table env /* 闭包的env, set/getenv就是操纵的它 */
C闭包的结构:
struct CClosure{ ClosureHeader; lua_CFunction f; TValue upvalue[1]; }
结构比较简单, f是一个知足 int lua_func(lua_State*) 类型的c函数
upvalue是建立C闭包时压入的upvalue, 类型是TValue, 能够得知, upvalue能够是任意的lua类型
Lua闭包结构:
struct LClosure{ ClosureHeader; strcut Proto* p; UpVal* upvals[1]; }
Proto的结构比较复杂, 这里先不作分析
统一的闭包结构, 一个联合体, 说明一个闭包要么是C闭包, 要么是lua闭包, 这个是用isC表识出来的.
union Closure{
CClosure c;
LClosure l;
}
纠结的闭包
为何你们叫闭包, 不叫它函数:
1. c 语言中的函数的定义: 对功能的抽象块;
2. lua对函数作了扩展:
a. 能够把几个值和函数绑定在一块儿, 这些值被称为upvalue.
ps: 可能有人以为c++的函数对象也能够把几个值和函数绑定起来啊, 是这样的, 可是这个问题就像是"在汇编中也能够实现面向对象呀"同样, lua从语言层面对upvalue提供了支持, 就像c++/java从语言层面提供了对类, 对象的支持同样, 固然大大的解放了咱们程序员的工做量, 并且配上lua动态类型, 更是让人轻松了很多.
b. 每一个函数能够和一个env(环境)绑定.
ps: 若是说上面的upvalue还能在c++中coding出来, 那么env 上下文环境这种动态语言中特有的东西c++就没有明显的对应结构了吧? 可能有人以为lua是c写的, 经过coding也能够实现, 好吧=.= , "能作和作"是两码事, 就想你能步行从北京到上海, 不代表你就必需要这么作. env是很是重要和有用的东西, 它能够轻松创造出一个受限的环境, 就是传说中的"沙盒", 我说的更通俗一点就是"一个动态名字空间机制". 这个先暂时不分析.
好了, 如今咱们看到
c 函数 { 功能抽象 }
lua 闭包 {功能抽象, upvalue, env}
重点: 闭包 == {功能抽象, upvalue, env}
看到这里, 你们都明白了, 若是把lua中的{功能抽象, upvalue, env}也称为函数, 不但容易引发你们的误解觉得它就是和c函数同样, 并且它确实不能很好的表达出lua函数的丰富内涵, 闭包, "闭" 是指的它是一个object, 一个看得见摸得着的东西, 不可分割的总体(first class); "包" 指的是它包含了功能抽象, upvalue, env. 这里一个颇有趣的事实就是, {功能抽象, upvalue, env}是不少动态语言的一个实现特征, 好比lua, javascript都有实现这样的结构, 它是先被实现出来, 而后冠以"闭包"这样一个名称. 因此, 你单单想去理解闭包这个词的话, 基本是没有办法理解的, 去网上查闭包, 没用, 你能查到的