深入理解Lua的闭包一:概念、应用和实现原理
本文首先通过具体的例子讲解了Lua中闭包的概念,然后总结了闭包的应用场合,最后探讨了Lua中闭包的实现原理。
闭包的概念
在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。另外在Lua的C 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
注意这里的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中闭包对应的源码实现以及调用的过程。
参考资料
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程序设计》
深入理解Lua的闭包一:概念、应用和实现原理的更多相关文章
- atitit.闭包的概念与理解attilax总结v2 qb18.doc
atitit.闭包的概念与理解attilax总结v2 qb18.doc 1.1. 闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数.1 2. #---- ...
- 两个函数彻底理解Lua中的闭包
本文通过两个函数彻底搞懂Lua中的闭包,相信看完这两个函数,应该能理解什么是Lua闭包.废话不多说,上 code: --[[************************************** ...
- lua 函数调用 -- 闭包详解和C调用
转自:http://www.cnblogs.com/ringofthec/archive/2010/11/05/luaClosure.html 这里, 简单的记录一下lua中闭包的知识和C闭包调用 前 ...
- Lua中闭包详解 来自RingOfTheC[ring.of.the.c@gmail.com]
这些东西是平时遇到的, 觉得有一定的价值, 所以记录下来, 以后遇到类似的问题可以查阅, 同时分享出来也能方便需要的人, 转载请注明来自RingOfTheC[ring.of.the.c@gmail.c ...
- JavaScript学习总结——我所理解的JavaScript闭包
一.闭包(Closure) 1.1.什么是闭包? 理解闭包概念: a.闭包是指有权限访问另一个函数作用域的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数,也就是创建一个内部函数,创建一 ...
- 深入理解JS的闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...
- 深入理解JavaScript的闭包特性如何给循环中的对象添加事件
初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...
- javascript中闭包的概念
这个是每个前端工程师绕不开的一个问题,网上各种资料很多,整个春节,我仔细研读了红皮经典中关于这一块的注释,加深了对这一块的理解. 有好几个概念需要重申一下.以下都是我的理解: 1. 闭包是javasc ...
- 我所理解的JavaScript闭包
目录 一.闭包(Closure) 1.1.什么是闭包? 1.2.为什么要用闭包(作用)? 1.2.1.保护函数内的变量安全. 1.2.2.通过访问外部变量,一个闭包可以暂时保存这些变量的上下文环境,当 ...
随机推荐
- spring框架的ioc
spring框架,主要思想可以用spring容器来理解 aop是一种对oop进行补充的软件设计思想,将和核心功能不相关代码抽象出来,由其他类完成.比如Singer类,我们可以定义一个Singer ...
- python多行代码简化
python中,可以把多行代码简化为一行,把for循环和if条件判断都集中到一行里来写,示例如下: >>> from nltk.corpus import stopwords > ...
- PHP 代 码 操 作 文 件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- PAT甲级1052 Linked List Sorting
题目:https://pintia.cn/problem-sets/994805342720868352/problems/994805425780670464 题意: 给定一些内存中的节点的地址,值 ...
- .NET Core HttpClient调用腾讯云对象存储Web API的"ERROR_CGI_PARAM_NO_SUCH_OP"问题
开门见山地说一下问题的原因:调用 web api 时请求头中多了双引号,请求体中少了双引号. 腾讯云提供的对象存储(COS)C# SDK 是基于 .NET Framework 用 WebRequest ...
- 阿里云不同账号之间相同地域的VPC网络互访
今天实际操作了一下,在这篇随笔中记录一下以备忘,主要参考阿里云帮助文档-不同账号下专有网络内网互通. 实现场景:账号A的VPC网络中的ECS访问账号B的VPC网络中的ECS与RDS(地域都在华东1), ...
- 使用djiango 创建网站
如果发现用户登录异常等情况请不要惊慌,换个浏览器就好了,谷歌有这个问题,也困扰我很久. 如果搭建过程,发现新建topic异常等现象,把不要惊慌,可能是你没有进行数据重新清理,请彻底格式化数据库就好了. ...
- [No000012D]WPF(5/7)依赖属性
介绍 WPF带来了很多传统 Windows 应用程序没有的新特性和选择.我们已经讨论了一些 WPF 的特性,是时候更进一步介绍其他特性了.当你读完这个系列之前的文章,我希望你已经或多或少地了解了 WP ...
- [No000010B]Git4/9-时光机穿梭
我们已经成功地添加并提交了一个readme.txt文件,现在,是时候继续工作了,于是,我们继续修改readme.txt文件,改成如下内容: Git is a distributed version c ...
- [No0000FA]C# 接口(Interface)
接口定义了所有类继承接口时应遵循的语法合同.接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分. 接口定义了属性.方法和事件,这些都是 ...