Lua中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值一样(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然可以定义函数。假设函数f2定义在函数f1中,那么就称f2为f1的内嵌(inner)函数,f1为f2的外包(enclosing)函数,外包和内嵌都具有传递性,即f2的内嵌必然是f1的内嵌,而f1的外包也一定是f2的外包。内嵌函数可以访问外包函数已经创建的所有局部变量,这种特性便是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或者upvalue(这个词多少会让人产生误解,因为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引用都会指向同一个地方。

  

posted on 2007-04-02 23:21 清源游民 阅读(2446) 评论(11)  编辑 收藏 引用 所属分类: Lua

FeedBack:
# re: 学习lua中的闭包
2007-04-03 08:59 | 梦在天涯
我还不之lua是什么东东那,哈哈,有机会学习哦  回复  更多评论
  
# re: 学习lua中的闭包
2007-04-27 11:39 | melodie
释我惑也,你是怎么分析出他的机制的?对于习惯了c/c++这种不可嵌套定义的语言的程序员来说,理解闭包真不太容易。我琢磨了半天,感觉它很类似于c++中的functor  回复  更多评论
  
# re: 学习lua中的闭包
2007-08-06 10:39 | 阿来
老大写得文章真不错!不顶下实在过意不去!  回复  更多评论
  
# re: 学习lua中的闭包
2007-09-23 22:49 | 绝对在乎你
这个东西比较有用,很多时候可以作为像C++的模板的方式使用。

最近一直在使用lua做应用,有空多交流!  回复  更多评论
  
# re: 学习lua中的闭包
2009-04-30 15:54 | zdy
不错的文章,顶一下  回复  更多评论
  
# re: 学习lua中的闭包
2009-08-21 03:38 | XX
很好啊,谢谢。看了更明白了  回复  更多评论
  
# re: 学习lua中的闭包
2009-10-29 11:05 | 炮灰九段
f1(1979)() <-> 1989
f1(1979)() <-> 1989
闭包是妙,不知道应用如何
  回复  更多评论
  
# re: 学习lua中的闭包
2010-09-06 22:47 | tinggo
找了好久,终于找到能够深刻解释闭包内部机制的文章。就算是3年后仍然是一篇很好的文章,但是请问楼主,Lua资料较少,您是怎么获得这些信息的?  回复  更多评论
  
# re: 学习lua中的闭包
2011-08-16 16:50 | xiaoboz
function f1(n)
local function f2()
print(n)
end
n = n + 10
return f2
end

g1 = f1(1979)
g1()--输出1989
g1(2000)--输出1989
  回复  更多评论
  
# re: 学习lua中的闭包
2011-10-07 02:50 |
顶,真的很不错  回复  更多评论
  
# re: 学习lua中的闭包
2011-10-07 02:53 |
@xiaoboz
因为g1()并不需要接受参数,而你传了一个参数,按照lua语法,你传的参数等于作废,所以不起作用
  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2007年4月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

留言簿(35)

随笔分类(78)

随笔档案(74)

文章档案(5)

搜索

  •  

最新评论

阅读排行榜

评论排行榜