在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
lua的闭包是个新概念,理解它需要个过程。今天在网上找了几篇文章看,不错,先记录下。 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()打印出正确的值。 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了)复制到自己管理的空间中以便将来访问。弄清晰了内部的秘密后,运行结果就不难解释了。 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的这种语义非常有价值,他使得闭包之间能不依赖全局变量进行通讯,从而使代码的可靠性大大提高。 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引用都会指向同一个地方。 —————————————————————————————————————————————————————————
在这段程序中,函数 inc_count 定义在函数 make_counter 内部,并作为 make_counter 的返回值。变量 count 不是 inc_count 内的局部变量,按照最内嵌套作用域的规则,inc_count 中的 count 引用的是外层函数中的局部变量 count。接下来的代码中两次调用 make_counter() ,并把返回值分别赋值给 c1 和 c2 ,然后又依次打印调用 c1 和 c2 所得到的返回值。 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 的作用域内。这看起来也是无法正常执行的。 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()) 这里使用了匿名函数。使用匿名函数能使代码得到简化,同时我们也不必挖空心思地去给一个不需要名字的函数取名字了。 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都有实现这样的结构, 它是先被实现出来, 然后冠以"闭包"这样一个名称. 所以, 你单单想去理解闭包这个词的话, 基本是没有办法理解的, 去网上查闭包, 没用, 你能查到的 |
请发表评论