Lua 闭包 环境 包管理

Variables

  • 访问一个不存在的全局变量得到nil
  • 释放一个全局变量只需将其赋值为nil,效果与未定义该变量一样
  • Lua 中的变量全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量
  • 局部变量比全局变量访问更快

Functions

1. 基本特性

  1. 多参数/返回值匹配:多余忽略,缺少用nil补足
  2. 可变参数:arg,table.pack,table.unpack
  3. 命名参数:参数的非顺序填充方式
  4. 正确处理尾调用:Lua能够高效正确处理尾调用,而不会导致栈溢出

2. 第一类函数

函数是第一类值,函数可以像其它值(string, number)样用于赋给变量,作为函数参数或返回值。函数定义实际上是一个赋值语句,将类型为function的变量赋给一个变量。

1
2
3
function foo.bar (x) return 2*x end
-- 等价于
foo.bar = function (x) return 2*x end

从这个角度来看,自然,与变量一样,Lua有全局函数和局部函数之分。

3. 词法闭包

词法闭包是指当在一个函数内部嵌套定义另一个函数时,内部函数体可以访问到外部函数的局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function newCounter()
local i = 0
return function() -- anonymous function
i = i + 1
return i
end
end
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
c2 = newCounter()
print(c2()) --> 1

-- 打印c1所有的upvalue 输出: i
local i=1
local up = debug.getupvalue(c1, i)
while(up ~= nil) do
print(up, " ")
i = i+1
up = debug.getupvalue(c1, i)
end
print(c1, c2) -- function: 0x7f8df1d02100 function: 0x7f8df1d02160

这种情况下,我们称i为匿名函数的外部局部变量(external local variable)或upvalue。在这里,newCounter函数返回了一个闭包(closure)。闭包是指一个函数和它的upvalues,闭包机制保证了即使upvalue已经超出了其作用域(newCounter返回),仍然能正确被闭包函数引用而不会释放(由Lua GC管理)。在上例中,我们说c1和c2是建立在同一个函数上,但作用于同一个局部变量(i)不同实例的两个不同的闭包。

通过打印的upvalues可以看到,只有被闭包函数引用的外部局部变量,才算作该闭包函数的upvalue,Lua会按照闭包函数引用的顺序为upvalue编号,该编号与upvalue定义顺序无关。

最后一点是,闭包函数都是动态生成的,这和Go中的闭包有所不同,Go的闭包函数是在编译时生成的,不同的闭包可以共享闭包函数(同一个函数地址)。Lua的闭包函数动态生成会一定程度地影响运行效率和内存占用。

Lua闭包除了用于高级函数,回调函数,迭代器等上下文环境中以外,在完全不同的上下文环境,可用于重定义或预定义函数,通过这种方法,可以为代码创建一个安全的执行环境(也叫沙箱,sandbox)。

Lua还提供了对C闭包的支持,每当你在Lua中创建一个新的C函数,你可以将这个函数与任意多个upvalues联系起来,每一个upvalue 可以持有一个单独的Lua值。当函数被调用的时候,可以通过假索引(lua_upvalueindex)自由的访问任何一个upvalues。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int counter (lua_State *L) {
double val = lua_tonumber(L, lua_upvalueindex(1));
lua_pushnumber(L, ++val); /* new value */
lua_pushvalue(L, -1); /* duplicate it */
lua_replace(L, lua_upvalueindex(1)); /* update upvalue */
return 1; /* return new value */
}

int newCounter (lua_State *L) {
lua_pushnumber(L, 0);
lua_pushcclosure(L, &counter, 1);
return 1;
}

C闭包与Lua闭包在概念上很相似,但有两点不同:

  1. C函数的upvalues是显示push到栈中的,而Lua则可通过闭包函数引用确定哪些是upvalues
  2. C闭包不能共享upvalues,每个闭包在栈中都有独立的变量集,但你可以通过将upvalues指向同一个table来实现共享

Chunk

Chunk是一系列语句,Lua执行的每一块语句,比如一个文件或者交互模式下的每一行都是一个Chunk。

当我们执行loadfile(“test.lua”)时,便将test.lua的内容编译后的Chunk作为一个函数返回,如果出现编译错误,则返回nil和错误信息。而dofile相当于:

1
2
3
4
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end

loadstring和dostring的关系类似,只是接收字符串而不是文件名为参数。

再看require,require和dofile完成同样的功能,但主要有几点不同:

  1. require会搜索Lua环境目录来加载文件
  2. require会判断文件是否已经加载而避免重复加载统一文件
  3. require可以用于加载C .so库,功能类似loadlib,参考这里

一个lua模块编译后的Chunk被作为匿名函数被执行,那么定义于模块中函数对模块局部变量的引用就形成了闭包,所以说Lua中的闭包真是无处不在。

Enviroment

Lua中的环境用table来表示,这简化了环境处理也带来了不少灵活性。

在Lua5.1及之前,Lua将环境本身存储在一个全局变量_G中,其中包含了全局变量,内置函数,内置模块等。我们在使用任何符号x时,如果在当前函数的局部变量和upvalues无法找到符号定义(PS: Lua查找变量定义的规则为:局部变量 -> 外部局部变量(upvalue) -> 全局变量),则会返回_G.x的值。由于_G是一个table,因此我们可以用它实现一些有意思的功能:

  1. 通过动态名字访问全局变量: _G[varname]
  2. 通过_G的metatable改变对未定义全局变量的读(__index)和写(__newindex)行为
  3. 通过setfenv改变指定函数的_G环境,制造函数执行的沙盒环境

现在再回头来看闭包,实际上,Lua闭包除了函数和upvalues,还包括函数环境,这三者组成了一个完整的执行沙盒。

在Lua5.2及之后,Lua取消了setfenv函数,用_ENV方案替代了_G方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- before Lua 5.1
function f()
setfenv(1, {})
-- code here
end

-- after Lua 5.2
function f()
local _ENV = {}
-- code here
end
or
function f()
local _ENV = {}
return function() ... end
end

_ENV有三个特性:

  1. 对全局变量x的引用,将转换为_ENV.x
  2. 每个编译后的Chunk,都有一个_ENV upvalue(哪怕并未使用),作为Chunk环境,并作用于其内定义的函数
  3. 在初始化时,_ENV=_G

除了以上三点外,_ENV和普通变量并无区别。因此我们可以直接通过local _ENV = {}来覆盖接下来的代码的环境。将环境(_ENV)作为一个普通的upvalue来处理,这样做的好处是简化了闭包的概念,闭包等于函数加upvalues(没有了全局变量_G),为闭包优化(如合并相同upvalues的闭包)提供更好的支持,同时也减少了setfenv(f, env)带来的不确定性和不安全性(函数的_ENV upvalue在闭包返回时就已经确定了)。

有_ENV还是一个table,因此对全局变量的访问控制等trick,仍然很容易实现。Lua目前仍然保留_G,但理解它们的别是比较重要的:

我们都知道Lua有一个全局注册表(Registry),其中包含整个Lua虚拟机的信息,在Registry的LUA_RIDX_GLOBALS索引中,保存了Globals(也就是_G),在创建Globals时,会生成_G._G=_G的自引用。在引入_ENV后,初始时,_ENV=_G,一旦编译器将_ENV放入Chunk的upvalue后,_ENV将作为普通upvalue被看待,因此我们可以对其重新赋值:

1
2
3
4
5
6
7
8
9
10
11
12
i = 1 -- 此时 _ENV.i == _G.i == 1
function f()
local _ENV={i=2, print=print, _G=_G}
print(i, _ENV.i, _G.i)
end

function g()
print(i, _ENV.i, _G.i)
end

f() -- 2 2 1
g() -- 1 1 1

因此,_ENV除了在创建时和_G都指向Registry[LUA_RIDX_GLOBALS]之外,和_G并没有直接联系(_G={}不会影响函数环境,_G.x=1仍然会影响注册表中的Globals),Lua5.2及之后的环境都由_ENV指定,_G出于历史原因保留,但实际上Lua并不在内部再使用:

Lua keeps a distinguished environment called the global environment. This value is kept at a special index in the C registry (see §4.5). In Lua, the global variable _G is initialized with this same value. (_G is never used internally.)

Packages

在Lua中,有闭包,灵活的table和环境管理,想要实现包管理有非常多的方法:

1. 基本方法

最简单的方法就是直接使用table和第一类函数特性:

1
2
3
4
5
complex = {}
function complex.new(r,i) ... end
function complex.add(c1,c2) ... end
...
return complex

执行这个Chunk后,便可以通过complex.xxx()使用complex中定义的API了。这种方案主要的缺点是包内包外的调用都必须加上前缀,并且不能很好地隐藏私有成员。

2. 局部函数

通过局部函数再导出的方式,我们可以解决包内调用前缀和隐藏私有成员(不导出即可)的问题。

1
2
3
4
5
local function new(r,i) ... end
local function add(c1,c2) ... end
...
complex = {new = new, add = add}
return complex

但这样容易忘了local,造成全局命名空间污染。

3. 独立环境

1
2
3
4
5
6
7
complex = {}
-- before Lua5.1: setfenv(1, complex)
local _ENV = complex

function new(r, i) ... end
function new(c1, c2) ... end
return complex

现在,包内所有全局符号new, add都会被转换为complex.new, complex.add,并且我们为包创建了一个独立沙盒环境,如果要在包内访问全局符号,也有多种方法:

1
2
3
4
5
6
7
-- 方案1: 保存老的全局环境 之后访问全局符号需要加上 _G.前缀
local _G = _G
-- 方案2: 通过metatable 效率低一些,并且外部可通过complex.print访问_G.print
setmetatable(complex, {__index = _G})
-- 方案3: 只导出要使用的函数 这种方法隔离型更好,并且更快
local sqrt = math.sqrt
local print = print