Lua 语言中的全局变量不需要声明就可以使用。虽然这种行为对小型程序来说较为方便,但在大型程序中一个简单的手误就可能造成难以发现的 bug 。不过,如果我们乐意的话,也可以改变这种行为。由于 Lua 语言将全局变量存放在一个普通的表中,所以可以通过元表来发现访问不存在的全局变量的情况。
一种方法是简单地检测所有对全局表中不存在的键的访问:
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 })
这段代码执行后,所有试图对不存在的全局变量的访问都将引发一个错误:
> print(a) stdin:1: attempt to read undeclared variable a
但是,我们应该如何声明一个新的变量呢?方法之一就是使用函数 rawset ,它可以绕过元方法:
function declare (name, initval) rawset(_G, name, initval or false) end
其中, or 和 false 保证新变量一定会得到一个不为 nil 的值。
另外一种更简单的方法是把对新全局变量的赋值限制在仅能在函数中进行,而代码段外层的代码则被允许自由赋值。
要检查赋值是否在主代码段中必须用到调试库。调用函数 debug.getinfo(2, "S") 将返回一个表,其中的字段 what 表示调用元方法的函数是主代码段还是普通的 Lua 函数还是 C 函数。使用该函数,可以将 __newindex 元方法重写:
__newindex = function (t, n, v) local w = debug.getinfo(1, "S").what if w ~= "main" and w ~= "C" then error("attempt to write to undeclared variable " .. n, 2) end rawset(t, n, v) end
这个新版本还可以接受来自 C 代码的赋值,因为一般 C 代码都知道自己究竟在做什么。
如果要测试一个变量是否存在,并不能简单地将它与 nil 比较。因为如果它为 nil ,那么访问就会引发一个错误。这时,应该使用 rawget 来绕过元方法:
if rawget(_G, var) == nil then -- 'var' 未被声明 ... end
正如前面所提到的,我们不允许值为 nil 的全局变量,因为值为 nil 的全局变量都会被自动地认为是未声明的。但是,要允许值为 nil 的全局变量也不难,只需要引入一个辅助表来保存已经声明变量的名称即可。一旦调用了元方法,元方法就会检查该表,看变量是否是未声明过的。最终的代码可能如下所示:
local declaredNames = {} 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 })
现在,即使像 x = nil 这样的赋值也能够声明全局变量了。
上述两种方法所导致的开销基本可以忽略不计。在第一种方法中,在普通操作期间元方法不会被调用。在第二种方法中,元方法只有当程序访问一个值为 nil 的变量时才会被调用。
Lua 语言发行版中包含一个 strict.lua 模块,它使用上述代码实现了对全局变量的检查。在编写 Lua 语言代码时使用它是一个良好的习惯。