环境
Lua 将所有的全局变量保存在一个常规的 table 中,这个 table 称为环境。Lua 将环境 table 自身保存在一个全局变量 _G
中(_G._G 等于 _G)。例如,使用以下代码就可以打印当前环境中所有全局变量的名称:
for n in pairs(_G) do print(n) end
具有动态名字的全局变量
对于访问和设置全局变量,通常赋值操作就可以了。不过,有时候会用到一些元编程的形式。比如,想访问一个全局变量,而它的名称却存储在另一个变量中,或者它的名称需要通过运行时的计算才能得到。为了获取这个变量的值,很多程序员都会试图写出这样的代码:
value = loadstring("return" .. varname)()
其中 varname 是变量的名称,如果名称是"a",那么上述代码就相当于执行语句"return a"返回 a 的值。然而,这段代码包含了一个新程序块的创建和编译。更好的方法是使用如下的代码:
value = _G[varname]
正因为环境是一个常规的 table ,所以可以用变量名作为 key 去直接索引它。
上面问题的一般化形式是,允许使用动态的字段名,如“io.read”或"a.b.c.d"。如果直接使用_G[“io.read”]则不会从 table io 中得到字段 read 的值。但我们可以写一个函数 getfield 来实现这个效果。这个函数是一个循环,从 _G 开始逐个字段地深入求值:
function getfield(f)
-- 从全局变量 table 开始
local v = _G
for w in string.gmatch(f, "[%w_]+") do
v = v[w]
end
return v
end
与之对应的设置字段的函数则稍微复杂些。像 a.b.c.d = v 这样的赋值,必须一直检索到 a.b.c,然后设置它的 d 字段的值为 v。下面这个函数 setfield 就完成了这项任务,并且创建路径中间不存在的 table。
function setfield(f, v)
-- 从全局变量 table 开始
local t = _G
for w, d in string.gmatch(f, "([%w_]+)(%.?)") do
print(w, d)
-- 如果不是最后一个字段
if d == "." then
-- 如果不存在就创建 table
t[w] = t[w] or {}
t = t[w]
-- 完成赋值
else
t[w] = v
end
end
end
调用上面的函数
setfield("t.x.y", 10)
就创建了两个全局 table,一个名为 “t”,一个作为 t 的 “x” 字段的值。
全局变量的声明
Lua 中的全局变量不需要声明就可以使用。对于小型程序来说很方便,但在大型程序中,一处笔误就可能造成难以发现的错误。但是这种情形可以改变。由于 Lua 将全局变量放在一个 table 中,我们就可以通过元表来改变其访问全局变量时的行为。
一种方法是简单地检测所有对全局 table 中不存在的 key 的访问:
setmetatable(_G, {
__newindex = function (_, n)
error("attempt to write to undeclared variable " .. n, 2)
end,
__index = function (_, n)
error("attempt to read undeclared variable " .. n, 2)
end
})
但是如何声明一个新的变量呢?一种方法是使用 rawset,它可以绕过元表:
function declare (name, initval)
rawset(_G, name, initval or false)
end
setmetatable(_G, {
__newindex = function (_, n)
error("attempt to write to undeclared variable " .. n, 2)
end,
__index = function (_, n)
error("attempt to read undeclared variable " .. n, 2)
end
})
declare("a", 1)
print(a) ---> 1
另一种更简单的方法是只允许在主程序块中对全局变量进行赋值,那么当声明变量 a = 1 时,只需检测此赋值是否在主程序块中。这可以使用 debug 库,调用 debug.getinfo(2, “S”) 将返回一个 table ,其中的 what 字段表示了调用元方法的函数是主程序块还是普通的 lua 函数,又或者是 C 函数。可以将 __newindex
元方法重写为:
setmetatable(_G, {
__newindex = function (t, n, v)
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undeclared variable " .. n, 2)
end
rawset(t, n ,v)
end,
__index = function (_, n)
error("attempt to read undeclared variable " .. n, 2)
end
})
为了测试一个变量是否存在,就不能简单地将它与 nil 比较。因为它为 nil,就会触发元表的 __index
方法,引发一个错误。这时可以用 rawget
来绕过元方法:
if rawget(_G, var) == nil then
-- 'var' 没有声明
end
正如前面提到的,不允许全局变量的值为 nil ,因为具有 nil 值的全局变量都会被自动地认为是未声明地,会触发元方法。要改变这个行为,就需要一个辅助的 table ,保存声明过的全局变量。一旦调用了元方法,元方法就检查该 table,以确定变量是否声明过,代码如下:
setmetatable(_G, {
__newindex = function (t, n, v)
if not declaredNames[n] then
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undeclared variable " .. n, 2)
end
declaredNames[n] = true
end
rawset(t, n ,v)
end,
__index = function (_, n)
if not declaredNames[n] then
error("attempt to read undeclared variable " .. n, 2)
else
return nil
end
end
})
这样,a = nil 这样赋值也可以起到声明全局变量的作用。
非全局的环境
关于环境的一大问题在于它是全局的,任何对它的修改都会影响程序的所有部分。例如,若为环境设置了一个元表用于控制全局变量的访问,那么整个程序都必须遵循这个规范。当使用某个库时,如果没有先声明就使用了全局变量,那么这个程序就无法运行了。
Lua5 对这个问题进行了改进,它允许每个函数拥有一个自己的环境来查找全局变量。
可以使用 setfenv
函数来改变一个函数的环境。该函数的参数是一个函数和一个新的环境 table。第一个参数除了可以指定为函数自身,还可以指定一个数字,以表示当前函数调用栈中的层数。数字1表示当前函数,数字2表示调用当前函数的函数,以此类推。
一旦改变了环境,所有的全局变量访问都会使用新的环境 table。如果新的 table 是空的,那么就会丢失所有的全局变量,包括 _G
。所以应该先将一些有用的值存入新的环境 table。例如:
a = 1
setfenv(1, {_G = _G}) -- 保存旧的环境
_G.print(a) --> nil
_G.print(g.a) --> 1
对于 Lua 来说,_G 只是一个普通的名字。当 Lua 创建最初的全局 table 时,只是将这个 table 赋予了全局变量 _G。setfenv
不会在新环境中设置这个变量。一般还是在新环境中使用 _G 这个名称来保存旧的环境。
另一种使用旧的环境的方法是使用继承:
a = 1
local newgt = {} -- 创建新环境
setmetatable(newgt, (__index = _G)) -- 设置新环境的元表
setfenv(1, newgt) -- 改变环境
print(a)
使用上述代码,任何 var = name 形式的赋值都发生在新的环境中,不会影响旧的环境。
每个函数及某些 closure 都有一个继承的环境。下面的代码演示了这种机制:
function factory()
return function () return a end
end
a = 3
f1 = factory()
f2 = factory()
print(f1()) --> 3
print(f2()) --> 3
setfenv(f1, {a = 10})
print(f1()) --> 10
print(f2()) --> 3
每个新创建的函数都继承了创建它的函数的环境,因此上例中的 closure 都共享一个全局环境。在调用 setfenv 后,就改变了 f1 函数的环境。