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

原文链接 https://blog.csdn.net/MaximusZhou/article/details/44280109

本文首先通过具体的例子讲解了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) 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 的踪影,而是直接使用闭包 fooupvaluet = Test(2015) 之后,t 这个闭包一定已把 n 妥善保存好了,之后 f1f2 如果在当前堆栈上未找到 n 就会自动到他们的外包闭包的 upvalue 引用数组中去找,并把找到的引用值拷贝到自己的 upvalue 引用数组中。所以 f1f2g1g2 引用的 upvalue 实际也是同一个变量,而刚才描述的搜索机制则确保了最后他们的 upvalue 引用都会指向同一个地方。

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

  • 作为高阶函数的参数,比如像 table.sort 函数的参数。

  • 创建其他的函数的函数,即函数返回一个闭包。

  • 闭包对于回调函数也非常有用。典型的例子就是界面上按钮的回调函数,这些函数代码逻辑可能是一模一样,只是回调函数参数不一样而已,即 upvalue 的值不一样而已。

  • 创建一个安全的运行环境,即所谓的沙盒(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

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

  • 实现迭代器。所谓迭代器就是一种可以遍历一种集合中所谓元素的机制。每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何进到下一个位置。闭包刚好适合这种场景。比如:
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)。值得注意的是,在 Lua5.2 之前,闭包中还包括一个对环境(environment)的引用,环境实质就是一个 table ,函数可以在该表中索引全局变量,从 Lua5.2 开始,取消了闭包中的环境,而引入一个变量 _ENV 来设置闭包环境。由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。

作用域(生成期)规则下的嵌套函数给如何实现内存函数存储外部函数的局部变量是一个众所周知的难题(The combination oflexical 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) 不同的是,声明该局部变量的函数都是直接在栈中操作它的。

20150315202047758.png

通过为每个变量最多创建一个 upvalue 并按需要重复利用这个 upvalue,保证了未决状态(未超过生命周期)的局部变量(pending vars)能够在闭包之间正确地共享。为了保证这种唯一性,Lua 维护这一条链表,该链表中每个节点对应一个打开的 upvalue(opend upvalue)结构,打开的 upvalue 是指当前正指向栈局部变量的 upvalue,如上图的未决状态的局部变量链表(the pending vars list)。当Lua创建一个新的闭包时,Lua 会遍历当前函数所有的外部的局部变量,对于每一个外部的局部变量,若在上面的链表中能找到该变量,则重复使用该打开的 upvalue ,否则,Lua 会创建一个新的打开的 upvalue ,并把它插入链表中。当局部变量离开作用域时(即超过变量生命周期),这个打开的upvalue就会变成关闭的 upvalue(closed upvalue),并把它从链表中删除,如上图右图所示意。一旦某个关闭的 upvalue 不再被任何闭包所引用,那么它的存储空间就会被回收。

一个函数有可能存取其更外层函数而非直接外层函数的局部变量。在这种情况下,当创建闭包时,这个局部变量可能不在栈中。Lua 使用 flat 闭包 (flat closures) 来处理这种情况。使用 flat 闭包,无论何时一个函数访问一个外部的局部变量并且该变量不在直接外部函数中,该变量也会进入直接外部函数的闭包中。当一个函数被实例化时,其对应闭包的所有变量要么在直接外部函数的栈中要么在直接外部函数的闭包中。第一部分举的最后一个例子就是这种情况。

你可能感兴趣的:([转]深入理解Lua的闭包一:概念、应用和实现原理)