深入理解Lua的闭包一:概念、应用和实现原理

    本文首先通过详细的样例解说了Lua中闭包的概念,然后总结了闭包的应用场合,最后探讨了Lua中闭包的实现原理。

  闭包的概念

    在Lua中,闭包(closure)是由一个函数和该函数会訪问到的非局部变量(或者是upvalue)组成的,当中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同一时候又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会訪问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中。函数是闭包一种特殊情况。另外在LuaC API中,全部关于Lua中的函数的核心API都是以closure来命名的。也可视为这一观点的延续。Lua中,函数是一种第一类型值(First-Class Value),它们具有特定的词法域(Lexical Scoping)

第一类型值表示函数与其它传统类型的值(比如数字和字符串类型)具有同样的权利。即函数能够存储在变量或table中,能够作为实參传递给其它函数。还能够作为其它函数的返回值。能够在执行期间被创建。在Lua中,函数与全部其它的值是一样都是匿名的,即他们没有名称。当讨论一个函数时(比如print)。实质上在讨论一个持有某个函数的变量。

比方:

function foo(x) print(x) end

实质是等价于

foo = function (x) print(x) end

因此一个函数定义实质就是一条赋值语句,这条语句创建了一种类型为“函数的值,并赋值给一个变量。能够将表达式function (x) <body> end 视为一种函数构造式,就像table的构造式{}一样。

    值得一提的是。C语言里面函数不能在执行期被创建。因此不是第一类值,只是有时他们被称为第二类值,原因是他们能够通过函数指针实现某些特性,比方经常显现的回调函数的影子。

    词法域是指一个函数能够嵌套在还有一个函数中,内部的函数能够訪问外部函数的变量。比方:

function f1(n)
   --函数參数n也是局部变量
   local function f2()
      print(n)   --引用外部函数的局部变量
   end
   return f2
end

g1 = f1(2015)
g1() -- 打印出2015

g2 = f1(2016)
g2() -- 打印出2016

注意这里的g1g2的函数体同样(都是f1的内嵌函数f2的函数体),但打印值不同。这是由于创建这两个闭包时。他们都拥有局部变量n的独立实例。其实,Lua编译一个函数时,会为他生成一个原型(prototype)。当中包括了函数体相应的虚拟机指令、函数用到的常量值(数,文本字符串等等)和一些调试信息。在执行时,每当Lua执行一个形如function...end 这种表达式时,他就会创建一个新的数据对象,当中包括了相应函数原型的引用及一个由全部upvalue引用组成的数组。而这个数据对象就称为闭包。由此可见。函数是编译期概念,是静态的,而闭包是执行期概念,是动态的。

g1g2的值严格来说不是函数而是闭包,而且是两个不同样的闭包,而每一个闭包能保有自己的upvalue值,所以g1g2打印出的结果当然就不同样了。

    这里的函数f2能够訪问參数n,而n是外部函数f1的局部变量。在f2中。变量n即不是全局变量也不是局部变量,将其称为一个非局部变量(non-local variable)或upvalueupvalue实际指的是变量而不是值,这些变量能够在内部函数之间共享。即upvalue提供一种闭包之间共享数据的方法。比方:
function Create(n)
   local function foo1()
      print(n)
   end
   local function foo2()
      n = n + 10
   end
   return foo1,foo2
end

f1,f2 = Create(2015)
f1() -- 打印2015

f2()
f1() -- 打印2025

f2()
f1() -- 打印2035

注意上面的样例中,闭包f1f2共享同一个upvalue了,这是由于当Lua发现两个闭包的upvalue指向的是当前堆栈上的同样变量时,会聪明地仅仅生成一个拷贝。然后让这两个闭包共享该拷贝。这样任一个闭包对该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(2015)
f1,f2 = t()
f1()        -- 打印2015

f2()
f1()        -- 打印2025

g1,g2 = t()
g1()        -- 打印2025

g2()
g1()        -- 打印2035

f1()        -- 打印2035

注意上面的运行的结果表明了闭包f1f2g1g2都共同拥有同一个upvalue,这是由于在创建inner1,inner2这两个闭包被创建时堆栈上根本未找到n的踪影,而是直接使用闭包fooupvalue

t = Test(2015)之后,t这个闭包一定已把n妥善保存好了,之后f1f2假设在当前堆栈上未找到n就会自己主动到他们的外包闭包的upvalue引用数组中去找,并把找到的引用值复制到自己的upvalue引用数组中。所以f1f2g1g2引用的upvalue实际也是同一个变量,而刚才描写叙述的搜索机制则确保了最后他们的upvalue引用都会指向同一个地方。

  闭包的应用
    在很多场合中闭包都是一种非常有价值的工具,主要有下面几个方面:

    I)作为高阶函数的參数,比方像table.sort函数的參数。

    II)创建其它的函数的函数,即函数返回一个闭包。

    III)闭包对于回调函数也很实用。典型的样例就是界面上button的回调函数,这些函数代码逻辑可能是一模一样,仅仅是回调函数參数不一样而已。即upvalue的值不一样而已。

    V)创建一个安全的执行环境,即所谓的沙盒(sandbox)。当执行一些未受信任的代码时就须要一个安全的执行环境。比方要限制一个程序訪问文件的话,仅仅须要使用闭包来重定义函数io.open就能够了:

do
  local oldOpen = io.open
  local accessOk = function(filename, mode)
	  <权限訪问检查>
  		end

  io.open = function (filename, mode)
		  if accessOk(filename, mode) then
			  return oldOpen(filename, mode)
		  else
			  return nil, "access denied"
		  end
	 end
end

经过又一次定义后,原来不安全的版本号保存到闭包的私有变量中。从而使得外部再也无法直接訪问到原来的版本号了。

    V)实现迭代器。所谓迭代器就是一种能够遍历一种集合中所谓元素的机制。每一个迭代器都须要在每次成功调用之间保持一些状态,这样才干知道它所在的位置及怎样进到下一个位置。

闭包刚好适合这样的场景。比方:

function values(t)
	local i = 0
	return function () i = i + 1 return t[i] end
end

t = {10, 20, 30}

iter = values(t)
while true do
	local element = iter()
	if element == nil then break end
	print(element)
end
闭包的实现原理

    当Lua编译一个函数时。它会生成一个原型(prototype)。原型中包含函数的虚拟机指令、函数中的常量(数值和字符串等)和一些调试信息。

在不论什么时候仅仅要Lua执行一个function .. end表达时,它都会创建一个新的闭包(closure)。

每一个闭包都有一个对应函数原型的引用以及一个数组,数组中每一个元素都是一个对upvalue的引用,能够通过该数组来訪问外部的局部变量(outer local variables

值得注意的是。在Lua 5.2之前,闭包中还包含一个对环境(environment)的引用。环境实质就是一个table,函数能够在该表中索引全局变量。从Lua 5.2開始,取消了闭包中的环境,而引入一个变量_ENV来设置闭包环境

由此可见,函数是编译期概念,是静态的。而闭包是执行期概念。是动态的。

    作用域(生成期)规则下的嵌套函数给怎样实现内存函数存储外部函数的局部变量是一个众所周知的难题(The combination of lexical scoping with first-class functions creates a well-known difficulty for accessing outer local variables)。

比方样例:

function add (x) 
	return function (y) 
		return x+y
	end
end

add2 = add(2)
print(add2(5))

    当add2被调用时,其函数体訪问了外部的局部变量x(在Lua中,函数參数也是局部变量)。然而。当调用add2函数时,创建add2add函数已经返回了,假设x在栈中创建,则当add返回时,x已经不存在了(即x的存储空间被回收了)。

    为了解决上面的问题。不同语言有不同的方法,比方python通过限定作用域、Pascal限制函数嵌套以及C语言则两者都不同意。在Lua中。使用一种称为upvalue结构来实现闭包。不论什么外部的局部变量都是通过upvalue来间接訪问。upvalue初始值是指向栈中,即变量在栈中的位置。例如以下图左边。当执行时,离开变量作用域时(即超过变量生命周期)。则会把变量拷贝到upvalue结构中(注意也仅仅是在此刻才执行这个操作),例如以下图右边。因为对变量的訪问都是通过upvalue结构中指针间接进行的,因此复制操作对不论什么读或写变量的代码来说都是没有影响的。与内部函数(inner functions)不同的是,声明该局部变量的函数都是直接在栈中操作它的。

    通过为每一个变量最多创建一个upvalue并按须要反复利用这个upvalue保证了未决状态(未超过生命周期)的局部变量(pending vars)可以在闭包之间正确地共享。为了保证这样的唯一性,Lua维护这一条链表。该链表中每一个节点相应一个打开的upvalueopend upvalue)结构,打开的upvalue是指当前正指向栈局部变量的upvalue,如上图的未决状态的局部变量链表(the pending vars list)。

Lua创建一个新的闭包时,Lua会遍历当前函数全部的外部的局部变量。对于每一个外部的局部变量,若在上面的链表中能找到该变量,则反复使用该打开的upvalue,否则,Lua会创建一个新的打开的upvalue。并把它插入链表中。当局部变量离开作用域时(即超过变量生命周期)。这个打开的upvalue就会变成关闭的upvalueclosed upvalue),并把它从链表中删除,如上图右图所看到的意。一旦某个关闭的upvalue不再被不论什么闭包所引用。那么它的存储空间就会被回收。

    一个函数有可能存取其更外层函数而非直接外层函数的局部变量。

在这样的情况下。当创建闭包时。这个局部变量可能不在栈中。

Lua使用flat 闭包(flat closures)来处理这样的情况。使用flat闭包。不管何时一个函数訪问一个外部的局部变量而且该变量不在直接外部函数中,该变量也会进入直接外部函数的闭包中。当一个函数被实例化时,其相应闭包的全部变量要么在直接外部函数的栈中要么在直接外部函数的闭包中。

第一部分举的最后一个样例就是这样的情况。下一篇文章将分析Lua中闭包相应的源代码实现以及调用的过程。

參考资料

http://hi.baidu.com/wplzjtyldobrtyd/item/a293ac3c243e70ff97f88d07
http://blog.sina.com.cn/s/blog_547c04090100qfps.html
http://www.douban.com/note/183992679/
http://en.wikipedia.org/wiki/Scope_(programming)#Lexical_scoping 
http://en.wikipedia.org/wiki/First-class_citizen
《Lua程序设计》

原文地址:https://www.cnblogs.com/yjbjingcha/p/7241490.html