为什么说 Lua 5.3 中没有全局变量了

过去笔者一直使用 Lua 5.1, 对 Lua 5.3 中的 _ENV 一知半解. 最近新项目中使用了 Lua 5.3, 于是特意研究了下. 这篇文章总结下 Lua 5.3 中的环境和全局变量, _ENV 的含义以及与之相关的用法.

Lua 变量的类型

Lua 中的变量可分为局部变量, 上值(upvalue)和全局变量. 经常使用 Lua 的同学应该都很熟悉, 举个例子:

1
2
3
4
5
local up = "str"
local function foo(a)
local b = a
print(up, b)
end

对于 function foo 来说, ab 是局部变量, up 是上值, print 则是全局变量.

局部变量和上值

一个变量是什么变量在编译时就能确定: 从当前函数依次向上寻找, 能在当前函数找到的变量为局部变量, 在上层函数找到的变量为上值, 都找不到的为全局变量. 下面是 Lua 寻找变量的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
Find variable with given name 'n'. If it is an upvalue, add this
upvalue into all intermediate functions.
*/
static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
if (fs == NULL) /* no more levels? */
init_exp(var, VVOID, 0); /* default is global */
else {
int v = searchvar(fs, n); /* look up locals at current level */
if (v >= 0) { /* found? */
init_exp(var, VLOCAL, v); /* variable is local */
if (!base)
markupval(fs, v); /* local will be used as an upval */
}
else { /* not found as local at current level; try upvalues */
int idx = searchupvalue(fs, n); /* try existing upvalues */
if (idx < 0) { /* not found? */
singlevaraux(fs->prev, n, var, 0); /* try upper levels */
if (var->k == VVOID) /* not found? */
return; /* it is a global */
/* else was LOCAL or UPVAL */
idx = newupvalue(fs, n, var); /* will be a new upvalue */
}
init_exp(var, VUPVAL, idx); /* new or old upvalue */
}
}
}

这段代码位于 lparser.c, 是 Lua 编译器的一部分. 自带的注释已经很详细了. 首先第 9 行会在当前函数的局部变量中寻找, 若找到则置为局部变量, 并存储它的索引(第几个局部变量), 否则尝试寻找上值. 在编译时每找到一个上值都会存起来, 包含每个上值的索引, 名字, 是否在栈中(即是否是上层函数的局部变量)等. 寻找上值时, 首先 16 行会在已有的上值中寻找, 若能找到, 24 行就将其置为上值并返回; 否则再在 18 行递归调用 singlevaraux 到上层函数中找. 若能找到, 则有两个结果: 一是找到它是上层函数的局部变量, 这能知道它是上层函数的第几个局部变量(局部变量索引); 二是找到它是上层函数的上值, 这能知道它是上层函数的第几个上值(上值索引). 然后在 22 行把这个索引存起来, 并标识它是上层函数的局部变量(在栈中)还是上值(不在栈中). 最后, 如果递归运行到第 7 行, 说明找遍嵌套当前函数的所有函数都找不到这个变量, 就认为它是一个全局变量, 在 20 行直接返回.

在运行时, 对于局部变量, 直接通过索引即可获得这个变量的值; 对于上值, 如果在栈中, 则通过索引在上层函数的局部变量中获得这个变量的值, 否则通过索引在上层函数的上值中获得这个变量的值. 也就是说, 对于局部变量和上值, Lua 在编译的时候就为它们编上号了, 运行时通过编号获取变量, 不 care 变量名.(所以不要再说变量名起短些运行效率高了)

全局变量

那么对于无法在局部变量和上值中找到的全局变量 Lua 是怎么做的呢? 这就是最有意思的地方了. 我们来看 singlevaraux 的调用者 singlevar. 当 Lua 编译时每遇到一个变量都会调用它:

1
2
3
4
5
6
7
8
9
10
11
12
static void singlevar (LexState *ls, expdesc *var) {
TString *varname = str_checkname(ls);
FuncState *fs = ls->fs;
singlevaraux(fs, varname, var, 1);
if (var->k == VVOID) { /* global name? */
expdesc key;
singlevaraux(fs, ls->envn, var, 1); /* get environment variable */
lua_assert(var->k != VVOID); /* this one must exist */
codestring(ls, &key, varname); /* key is variable name */
luaK_indexed(fs, var, &key); /* env[varname] */
}
}

首先 singlevar 调用 singlevaraux 寻找变量, 如果找不到, 则第 6 行开始处理全局变量. 这里它首先调用 singlevaraux 传入了 ls->envn. 查查代码就能发现, ls->envn 的值为 "_ENV", 也就是它要寻找一个名为 _ENV 的变量. 接着在第 9 行将变量名作为字符串存储在常量区, 然后第 10 行生成了一个表查找指令, 在变量 _ENV 中查找键为该变量名的值.

也就是说对于每个全局变量 var 来说, Lua 都处理成 _ENV.var. 那么在我们没有手动定义的情况下, 这个 _ENV 变量是从何而来的呢? 我们可以找到它被设置的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* lparser.c */
static void mainfunc (LexState *ls, FuncState *fs) {
BlockCnt bl;
expdesc v;
open_func(ls, fs, &bl);
fs->f->is_vararg = 1; /* main function is always declared vararg */
init_exp(&v, VLOCAL, 0); /* create and... */
newupvalue(fs, ls->envn, &v); /* ...set environment upvalue */
luaX_next(ls); /* read first token */
statlist(ls); /* parse main body */
check(ls, TK_EOS);
close_func(ls);
}

/* lapi.c */
LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
const char *chunkname, const char *mode) {
ZIO z;
int status;
lua_lock(L);
if (!chunkname) chunkname = "?";
luaZ_init(L, &z, reader, data);
status = luaD_protectedparser(L, &z, chunkname, mode);
if (status == LUA_OK) { /* no errors? */
LClosure *f = clLvalue(L->top - 1); /* get newly created function */
if (f->nupvalues >= 1) { /* does it have an upvalue? */
/* get global table from registry */
Table *reg = hvalue(&G(L)->l_registry);
const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
/* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
setobj(L, f->upvals[0]->v, gt);
luaC_upvalbarrier(L, f->upvals[0]);
}
}
lua_unlock(L);
return status;
}

当调用 load, loadfiledofile 等方法的时候, 都会调用到 lua_load, 这会加载代码并进行编译. 先在 23 行调用 luaD_protectedparser 编译代码, 这会调用到 mainfunc 编译 main 函数(或者说 chunk). 注意到第 8 行, 它会给 main 函数设置一个名为 _ENV 的上值变量. 然后在 31 行将全局 table 设为 main 函数的第一个上值, 也是它唯一的上值, 即 _ENV. 这个全局 table 在 Lua 虚拟机初始化的时候就被创建, 里面包含各种标准库函数.

也就是说在我们访问全局变量的时候, 实际上是在访问上值 _ENV 中的键值; 由于嵌套定义的函数会继承其上层函数的上值, 这使得所有的函数访问到的 _ENV 变量都是同一个变量, 看上去就像共用相同的全局变量了. 下面这段代码举了个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local function foo()
a = 1
(function()
b = "str"
end)()
end

local function bar()
local _ENV = {}
c = 3.14
(function()
d = "str"
end)()
end

foo()
bar()

print(a, b, c, d) -- 1 str nil nil

对于函数 foo 来说, 它在第 2 行实际执行的是 _ENV.a = 1. 这里的 _ENV 继承自 main 函数的上值, 因此在最后能打印出 a 为 1. b 也是同样的道理. 但对于函数 bar 来说, 它执行到第 10 行和 第 12 行的时候, 实际是为它在第 9 行定义的局部变量设置键值, 因此在最后不能打印出来.

_G: 那我呢?

既然 Lua 5.3 中的全局变量实际上是 _ENV 中的键值, 那 _G 是什么呢? 事实上, _G_ENV 中的一个键, 其值指向 _ENV 本身. 也就是有 _ENV._G = _ENV. 这样做完全是为了兼容以前的写法, _G 已经失去它以前的含义了. 即使你执行 _G = nil 也是没关系的, 只要你不手动访问 _G, 就不会有任何影响.

BTW, LuaJIT 的用户习惯在文件头写上 local _G = _G 并在使用全局变量时显式指定 _G.var, 因为这样能更快些. 在 Lua 5.3 中, 这样做就没有任何意义了. 并且如果你忘记在文件头加上 local _G = _G 的话还会更慢, 因为这样做相当于 _ENV._G.var 多了一次 table 查找.

总结

Lua 5.3 把非局部, 非上值的变量 var 定义为 _ENV.var; 并且为 main 函数设置了一个初始的上值 _ENV 等于一个 table. 除此之外没有做任何其他的工作就实现了全局变量的效果. 因此我们可以说 Lua 5.3 中的全局变量实际上只是一个语法糖. 这种设计简洁精美, 非常漂亮. 不得不说 Lua 是一门贯彻了 Less is batter than more 的语言; 它的源码也是一座宝库, 值得每个人学习.


为什么说 Lua 5.3 中没有全局变量了
https://luyuhuang.tech/2020/03/20/lua53-environment.html
Author
Luyu Huang
Posted on
March 20, 2020
Licensed under