lua的闭包概念理解与介绍

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
——————————————————————————————————————————
       lua中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值相同(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然能定义函数。假设函数f2定义在函数f1中,那么就称f2为f1的内嵌(inner)函数,f1为f2的外包(enclosing)函数,外包和内嵌都具有传递性,即f2的内嵌必然是f1的内嵌,而f1的外包也一定是f2的外包。内嵌函数能访问外包函数已创建的所有局部变量,这种特性便是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或upvalue。试看如下代码:

 
  1. function f1(n)

  2. -- 函数参数也是局部变量

  3.  
  4.  
  5. local function f2()

  6. print(n) -- 引用外包函数的局部变量

  7. end

  8. return f2

  9. end

  10.  
  11.  
  12. g1 = f1(1979)

  13. g1() -- 打印出1979

  14. g2 = f1(500)

  15. 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函数改成:

 
  1. function f1(n)

  2. local function f2()

  3. print(n)

  4. end

  5. n = n + 10

  6. return f2

  7. end

  8.  
  9.  
  10. g1 = f1(1979)

  11. 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还能为闭包之间提供一种数据共享的机制。试看下例:

 
  1. function Create(n)

  2. local function foo1()

  3. print(n)

  4. end

  5. local function foo2()

  6. n = n + 10

  7. end

  8. return foo1,foo2

  9. end

  10.  
  11. f1,f2 = Create(1979)

  12. f1() -- 打印1979

  13. f2()

  14. f1() -- 打印1989

  15. f2()

  16. 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就已不在堆栈上的情况也有可能发生,这是因为内嵌函数能引用更外层外包函数的局部变量:

 
  1. function Test(n)

  2. local function foo()

  3. local function inner1()

  4. print(n)

  5. end

  6. local function inner2()

  7. n = n + 10

  8. end

  9. return inner1,inner2

  10. end

  11. return foo

  12. end

  13.  
  14. t = Test(1979)

  15. f1,f2 = t()

  16. f1() -- 打印1979

  17. f2()

  18. f1() -- 打印1989

  19. g1,g2 = t()

  20. g1() -- 打印1989

  21. g2()

  22. g1() -- 打印1999

  23. 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将帮助程式员善用这种能力。
——————————————————————————————————————————————————————————
——————————————————————————————————————————————————————————
什么是闭包?
闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定义有很多种说法,这些说法大体可以分为两类:
一种说法认为闭包是符合一定条件的函数,比如参考资源中这样定义闭包:闭包是在其词法上下文中引用了自由变量的函数。
另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。比如参考资源中就有这样的的定义:在实现深约束时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。
这两种定义在某种意义上是对立的,一个认为闭包是函数,另一个认为闭包是函数和引用环境组成的整体。虽然有些咬文嚼字,但可以肯定第二种说法更确切。闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:
函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
函数可以嵌套定义,即在一个函数内部可以定义另一个函数。
这些概念上的解释很难理解,显然一个实际的例子更能说明问题。Lua 语言的语法比较接近伪代码,我们来看一段 Lua 的代码:
清单 1. 闭包示例1 

 
 
  1. function make_counter()

  2. local count = 0

  3. function inc_count()

  4. count = count + 1

  5. return count

  6. end

  7. return inc_count

  8. end

  9. c1 = make_counter()

  10. c2 = make_counter()

  11. print(c1())

  12. print(c2())

 


在这段程序中,函数 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  

 
  1. function do10times(fn)

  2. for i = 0,9 do

  3. fn(i)

  4. end

  5. end

  6. sum = 0

  7. function addsum(i)

  8. sum = sum + i

  9. end

  10. do10times(addsum)

  11. print(sum)

这里我们看到,函数 addsum 被传递给函数 do10times,被并在 do10times 中被调用10次。不难看出 addsum 实际的执行点在 do10times 内部,它要访问非局部变量 sum,而 do10times 并不在 sum 的作用域内。这看起来也是无法正常执行的。
这两种情况所面临的问题实质是相同的。在这样的语言中,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个整体。当把这个整体当作函数调用时,先把其中的引用环境覆盖到当前的引用环境上,然后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由引用环境与函数代码组成的实体就是闭包。当然如果编译器或解释器能够确定一个函数在定义和运行时的引用环境是相同的,那就没有必要把引用环境和代码组合起来了,这时只需要传递普通的函数就可以了。现在可以得出这样的结论:闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。
再次观察上面两个例子会发现,代码中并没有通过名字来调用函数 inc_count 和 addsum,所以他们根本不需要名字。以第一段代码为例,它可以重写成下面这样:
清单 3. 闭包示例3                

 
  1. function make_counter()

  2. local count = 0

  3. return function()

  4. count = count + 1

  5. return count

  6. end

  7. end

  8.  
  9. c1 = make_counter()

  10. c2 = make_counter()

  11. print(c1())

  12. print(c2())

这里使用了匿名函数。使用匿名函数能使代码得到简化,同时我们也不必挖空心思地去给一个不需要名字的函数取名字了。
上面简单地介绍了闭包的原理,更多的闭包相关的概念和理论请参考参考资源中的"名字,作用域和约束"一章。
一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:
函数是一阶值;
函数可以嵌套定义;
可以捕获引用环境,并
把引用环境和函数代码组成一个可调用的实体;
允许定义匿名函数;
这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。另外需要注意,有些语言使用与函数定义不同的语法来定义这种能被传递的"函数",如 Ruby 中的 Block。这实际上是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。
借用一个非常好的说法来做个总结:对象是附有行为的数据,而闭包是附有数据的行为。
——————————————————————————————————————————————————————————
——————————————————————————————————————————————————————————

这些东西是平时遇到的, 觉得有一定的价值, 所以记录下来, 以后遇到类似的问题可以查阅, 同时分享出来也能方便需要的人, 转载请注明来自RingOfTheC[[email protected]]
这里, 简单的记录一下lua中闭包的知识和C闭包调用前提知识: 在lua api小记2中已经分析了lua中值的结构, 是一个 TValue{value, tt}组合, 如果有疑问, 可以去看一下

 一些重要的数据结构

     lua中有两种闭包, c闭包和lua闭包

    两种闭包的公共部分: 

 
  1. #define ClosureHeader CommonHeader; lu_byte isC; lua_byte nupvalues; GCObject* gclist; struct Table env

  2. /*是否是C闭包*/ /*upval的个数*/ /* 闭包的env, set/getenv就是操纵的它 */


    C闭包的结构        

 
  1. struct CClosure{

  2. ClosureHeader;

  3. lua_CFunction f;

  4. TValue upvalue[1];

  5. }

      结构比较简单, f是一个满足 int lua_func(lua_State*) 类型的c函数

      upvalue是创建C闭包时压入的upvalue, 类型是TValue, 可以得知, upvalue可以是任意的lua类型

     Lua闭包结构       

 
  1. struct LClosure{

  2. ClosureHeader;

  3. strcut Proto* p;

  4. UpVal* upvals[1];

  5. }

      Proto的结构比较复杂, 这里先不做分析 

   统一的闭包结构, 一个联合体, 说明一个闭包要么是C闭包, 要么是lua闭包, 这个是用isC表识出来的.     

 
  1. union Closure{

  2. CClosure c;

  3. LClosure l;

  4. }

纠结的闭包

       为什么大家叫闭包, 不叫它函数, 它看起来就是函数啊? 为什么要发明一个"闭包"这么一个听起来蛋疼的词呢? 我也纠结在这里好久了, 大概快一年半了吧~~~=.=我比较笨~~~随着看源码, 现在想通了, 拿出一些的自己在研究过程中的心得[尽量的通俗易懂]:

       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都有实现这样的结构, 它是先被实现出来, 然后冠以"闭包"这样一个名称. 所以, 你单单想去理解闭包这个词的话, 基本是没有办法理解的, 去网上查闭包, 没用, 你能查到的就是几个用闭包举出的例子, 看完以后保证你的感觉是"这玩意挺神秘的, 但是还是不懂什么是闭包", 为什么不懂?  因为它指的是一种实现结构特征, 是为了实现动态语言中的函数first class和上下文概念而创造出来的.

       宁可多说几句, 只要对加深理解有好处就行, 有这样两个个句子"我骑车去买点水果" "我用来闭包{功能抽象, upvalue, env}实现动态语言中的函数first class和上下文概念" , 闭包和"骑车"都是你达到目地的一种手段, 为了买水果你才想了"骑车"这样一个主意, 并不是为了骑车而去买水果. 只把把眼睛盯在骑车上是不对的, 它只是手段.

向lua中注册c函数的过程是通过lua_pushcclosure(L, f, n)函数实现的

       流程:  1. 创建一个 sizeof(CClosure) + (n - 1) * sizeof(TValue)大小的内存, 这段内存是 CClosure + TValue[n], 并做gc簿记[这点太重要了, 为什么lua要控制自己世界中的所有变量, 就是因为它要做gc簿记来管理内存],  isC= 1 标示其是一个C闭包.

             2. c->f = f绑定c函数.    ---------  闭包.功能抽象 = f

             3. env = 当前闭包的env[这说明了被创建的闭包继承了创建它的闭包的环境].  ----------- 闭包.env = env

             4. 把栈上的n个元素赋值到c->upvalue[]数组中, 顺序是越先入栈的值放在upvalue数组的越开始位置, c->nupvalues指定改闭包upvalue的个数.  ---------- 闭包.upvalue = upvalue

             5. 弹出栈上n个元素, 并压入新建的Closure到栈顶.

       整个流程是比较简单的, 分配内存, 填写属性, 链入gc监控, 绑定c函数, 绑定upvalue, 绑定env一个C闭包就ok了, 请结合上面给的闭包的解释, 很清楚了.

现在来解析这个C闭包被调用的过程[注意, 这里只涉及C闭包的调用]

       lua 闭包调用信息结构:          

 
  1. struct CallInfo {

  2. StkId base; /* base for this function */ ---- 闭包调用的栈基

  3. StkId func; /* function index in the stack */ ---- 要调用的闭包在栈上的位置

  4. StkId top; /* top for this function */ ---- 闭包的栈使用限制, 就是lua_push*的时候得看着点, push太多就超了, 可以lua_checkstack来扩

  5. const Instruction *savedpc; ---- 如果在本闭包中再次调用别的闭包, 那么该值就保存下一条指令以便在返回时继续执行

  6. int nresults; /* expected number of results from this function */ ---- 闭包要返回的值个数

  7. int tailcalls; /* number of tail calls lost under this entry */ ---- 尾递归用, 暂时不管

  8. }

        从注释就可以看出来, 这个结构是比较简单的, 它的作用就是维护一个函数调用的有关信息, 其实和c函数调用的栈帧是一样的, 重要的信息base –> ebp, func –> 要调用的函数的栈index, savedpc –> eip, top, nresults和tailcalls没有明显的对应.

        在lua初始化的时候, 分配了一个CallInfo数组, 并用L->base_ci指向该数组第一个元素, 用L->end_ci指向该数组最后一个指针, 用L->size_ci记录数组当前的大小, L->ci记录的是当前被调用的闭包的调用信息.

         下面讲解一个c闭包的调用的过程:

        情景: c 函数 int lua_test(lua_State* L){

                          int a = lua_tonumber(L, 1);

                          int b = lua_tonumber(L, 2);

                          a = a + b;

                          lua_pushnumber(L, a);

                 }

                 已经注册到了lua 中, 形成了一个C闭包, 起名为"test", 下面去调用它

                 luaL_dostring(L, "c = test(3, 4)")

         1. 首先, 我们把它翻译成对应的c api

             lua3

                                1. 最初的堆栈

               lua_getglobal(L, “test”)

               lua_pushnumber(L, 3)

               lua_pushnumber(L, 4)      

             lua2

                              2. 压入了函数和参数的堆栈

               lua_call(L, 2, 1)

             lua5

                               3. 调用lua_test开始时的堆栈

             lua4

                               4. 调用结束的堆栈

               lua_setglobal(L, “c”)     

             lua3

                               5. 取出调用结果的堆栈

        我们重点想要知道的是lua_call函数的过程

            1. lua的一致性在这里再一次的让人震撼, 不管是dostring, 还是dofile, 都会形成一个闭包, 也就是说, 闭包是lua中用来组织结构的基本构件, 这个特点使得lua中的结构具有一致性, 是一种简明而强大的概念.

            2. 根据1, a = test(3, 4)其实是被组织成为一个闭包放在lua栈顶[方便期间, 给这个lua闭包起名为bb], 也就说dostring真正调用的是bb闭包, 然后bb闭包执行时才调用的是test

         [保存当前信息到当前函数的CallInfo中]

            3. 在调用test的时刻, L->ci记载着bb闭包的调用信息, 所以, 先把下一个要执行的指令放在L->ci->savedpc中, 以供从test返回后继续执行.

            4. 取栈上的test C闭包 cl, 用 cl->isC == 1断定它的确是一个C闭包

         [进入一个新的CallInfo, 布置堆栈]

            5. 从L中新分配一个CallInfo ci来记录test的调用信息, 并把它的值设置到L->ci, 这表明一个新的函数调用开始了, 这里还要指定test在栈中的位置, L->base = ci->base = ci->func+1, 注意, 这几个赋值很重要, 导致的堆栈状态由图2转化到图3, 从图中可以看出, L->base指向了第一个参数, ci->base也指向了第一个参数, 所以在test中, 我们调用lua_gettop函数返回的值就是2, 因为在调用它的时候, 它的栈帧上只有2个元素, 实现了lua向c语言中传参数.

        [调用实际的函数]

            6. 安排好堆栈, 下面就是根据L->ci->func指向的栈上的闭包(及test的C闭包), 找到对应的cl->c->f, 并调用, 就进入了c函数lua_test

        [获取返回值调整堆栈, 返回原来的CallInfo]

            7. 根据lua_test的返回值, 把test闭包和参数弹出栈, 并把返回值压入并调整L->top

            8. 恢复 L->base, L->ci 和 L->savedpc, 继续执行.

        总结: 调用一个新的闭包时 1. 保存当前信息到当前函数的CallInfo中 2. 进入一个新的CallInfo, 布置堆栈  3. 调用实际的函数  4. 获取返回值调整堆栈, 返回原来的CallInfo

猜你喜欢

转载自blog.csdn.net/liu943367080/article/details/88951964