深入理解Lua的闭包一:概念和应用_cbbbc的博客-CSDN博客

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

闭包的概念

在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。另外在Lua的C API中,所有关于Lua中的函数的核心API都是以closure(而非function)来命名的,也可视为这一观点的延续。在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

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

这里的函数f2可以访问参数n,而n是外部函数f1的局部变量。在f2中,变量n即不是全局变量也不是局部变量,将其称为一个非局部变量(non-local variable)或upvalue。upvalue实际指的是变量而不是值,这些变量可以在内部函数之间共享,即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

注意上面的例子中,闭包f1和f2共享同一个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

注意上面的执行的结果表明了闭包f1、f2、g1和g2都共有同一个upvalue,这是因为在创建inner1,inner2这两个闭包被创建时堆栈上根本未找到n的踪影,而是直接使用闭包foo的upvalue。t = Test(2015)之后,t这个闭包一定已把n妥善保存好了,之后f1、f2如果在当前堆栈上未找到n就会自动到他们的外包闭包的upvalue引用数组中去找,并把找到的引用值拷贝到自己的upvalue引用数组中。所以f1、f2、g1和g2引用的upvalue实际也是同一个变量,而刚才描述的搜索机制则确保了最后他们的upvalue引用都会指向同一个地方。

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

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

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

III)闭包对于回调函数也非常有用。典型的例子就是界面上按钮的回调函数,这些函数代码逻辑可能是一模一样,只是回调函数参数不一样而已,即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函数时,创建add2的add函数已经返回了,如果x在栈中创建,则当add返回时,x已经不存在了(即x的存储空间被回收了)。

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

通过为每个变量最多创建一个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中闭包对应的源码实现以及调用的过程。

深入理解Lua的闭包:概念和应用相关推荐

  1. 两个函数彻底理解Lua中的闭包

    本文通过两个函数彻底搞懂Lua中的闭包,相信看完这两个函数,应该能理解什么是Lua闭包.废话不多说,上 code: 1 --[[************************************ ...

  2. 原来 JS 也支持跟 Lua 语意一样的内嵌函数的闭包概念

    原来 JS 也支持跟 Lua 语意一样的内嵌函数的闭包概念. 我是从这里看来的: http://blog.dreambrook.com/soloist/archive/2005/03/13/526.a ...

  3. 从λ演算到函数式编程聊闭包(1):闭包概念在Java/PHP/JS中形式

    什么是闭包 如果让谷哥找一下"闭包"这个词,会发现网上关于闭包的文章已经不计其数 维基百科上对闭包的解释就很经典: 在计算机科学中,闭包(Closure)是词法闭包(Lexical ...

  4. 深入理解javascript的闭包

    闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...

  5. 理解 Lua 的那些坑爹特性

    Lua 那些坑爹的特性 来源 https://blog.lilydjwg.me/2012/12/29/lua-caveats.36879.html 协程只能在 Lua 代码中使用 协程(corouti ...

  6. 深入理解JavaScript的闭包特性如何给循环中的对象添加事件

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  7. 理解系统底层的概念是多么重要

    理解系统底层的概念是多么重要                                --趋势科技邹飞评<程序员的自我修养>   关于<程序员的自我修养>这本书,最初是在 ...

  8. 理解五个基本概念,让你更像机器学习专家

    理解五个基本概念,让你更像机器学习专家 https://www.jianshu.com/p/ca37ea88a757 摘要: 这篇文章主要讲述了机器学习的相关内容,阐述了机器学习的主要意义和形成过程. ...

  9. 深入理解正则表达式环视的概念与用法

    在<深入理解正则表达式高级教程-环视>中已经对环视做了简单的介绍,但是,可能还有一些读者比较迷惑,今天特意以专题的形式,深入探讨一下正则表达式的环视的概念与用法. 深入理解正则表达式环视的 ...

最新文章

  1. 激发企业大“智慧” | 深度赋能AI全场景 揭秘你不知道的移动云
  2. 检测网络耗时_无人机替代桥梁检测车检测桥梁的可行性分析
  3. redis 秒杀成功 mysql_如何使用Redis实现秒杀
  4. python 定义数组
  5. django-django的开发流程
  6. 双路由器双小型交换机组建公司网络,2个公网IP上网案例(转载)
  7. python100例详解-几个小例子给你讲解Python中类的描述符
  8. 4.3 调度核心组件
  9. linux sed批量更改文件,Linux利用sed批量修改文件名(示例代码)
  10. SCM供应链管理系统介绍:企业SCM供应链系统应用领域、优势、功能详解
  11. 七.OpenCv图像轮廓
  12. lubuntu输入法设置_Ubuntu 设置中文输入法
  13. Arduino环境使用TM1638扩展版
  14. PPT画图保存时自动压缩图片问题
  15. 如果因为溢出导致了结果为负,那么逻辑上真正的结果必然为正
  16. 数字统计之统计页码数字出现的次数
  17. NXP JN5169使用定时器进行PWM输出和定时功能
  18. Druid监控页面配置用户密码、去除Ad
  19. 下载 Internet Explorer 11(脱机安装程序)
  20. 【EI检索】第二届传感器与信息技术国际学术会议(ICSI2022)

热门文章

  1. 2017年支付宝五福活动的python生福脚本。
  2. 计算机组成原理江爱文,计算机组成原理
  3. 移动端顶部状态栏显示及修改背景色
  4. 【华为机试真题Java】用连续自然数之和来表达整数
  5. 微信读书从本地导入书籍失败
  6. 管理日常工作、生活琐事的待办事项提醒工具便签
  7. Translation Equivariance
  8. 什么叫视听思维,如何训练视听思维?(周传基)
  9. 你问西湖水用计算机弹,你问西湖水偷走她的几分美是什么歌,恋人心歌曲介绍...
  10. 编译openwrt n2n遇到错误