Lua将所有的全局变量保存在一个常规的table中,这个table称为"环境(enviroment)".
这种组织结构的优点在于,其一、不需要再为全局变量创造一种新的数据结构,因此简化了Lua的内部实现。
另一个优点是,可以像其他table一样操作这个table。为了便于实施这种操作,Lua将环境table自身保存在一
个全局变量_G中。示例 —— 打印当前环境中所有全局变量的名称:
for n in pairs(_G) do print(n) end
14.1 具有动态名字的全局变量
对于访问和设置全局变量,通常赋值操作就可以。有时也会用到元编程的形式。如当操作一个全局变量时,
而它的名称却存储在另一个变量中,或者需要通过运行时的计算才能得到。因为环境是一个常规的table,
我们可以使用一个key去索引它,如:
value = _G[varname] -- 获取全局变量varname的值
_G[varname] = value -- 设置全局变量varname的值
上面问题的一般化形式是,允许使用动态的字段名,如"io.read" 或 "a.b.c.d"。如果直接写_G["io.read"]不会
从table io 中得到字段read。但可以写一个函数getfield来实现这个效果。这个函数是一个循环,从_G开始逐
个字段深入求值:
function getfield(f)
local v = _G
for w instring.gmatch(f, "[%w_]+") do
v = v[w]
end
return v
end
通过string库中的gmatch函数来遍历f中所有的单词。
设置字段的函数必须一直检索到最后一个名称,然后分别进行操作。下面setfield函数就完成了这项任务,并
且创建路径中间那些不存在的table:
function setfield(f, v)
local t = _G
for w, d instring.gmatch(f, "([%w_]+)(%.?)") do
if d =="." then
t =t[w]
else
t[w] = v
end
end
end
上例中用到了一种字符串模式,通过这种模式可以将字段名捕获到变量w中,并将一个可选的句号捕获到d中。
14.2 全局变量的声明
Lua中全局变量不需要声明就可以使用。由于Lua将全局变量存放在一个普通的table中,因此可以通过元表来
修改其访问全局变量的行为。
一种方法是简单地检测所有对全局table中不存在的key的访问:
setmetatable(_G, {__newindex = function(_, n)
error("attempt to write toundeclared variable"..n, 2)
end,
__index = function(_,n)
error("attemptto read undeclared variable"..n, 2)
执行这段代码后,所有对全局table中不存在的key的访问都将引发一个错误。这时声明新变量的方法有两种,
其一是使用rawset,它可以绕过元表:
function declare(name, initval)
rawset(_G, name,initval or false)
end
另外一种更简单的方法是只允许在主程序块中对全局变量进行赋值,当声明全局变量时只需检查此赋值是否
在主程序块中。可以使用debug库,调用debug.getinfo(2,"S")将返回一个table,这个table中的字段what表示
调用元方法的函数是主程序块还是普通函数,又或是C函数。因此可以将__newindex元方法重写为:
__newindex = function(t, n, v)
local w =debug.getinfo(2, "S").what
if w ~="main" and w ~= "C" then
error("attemptto write to undeclared variable"..n, 2)
end
rawset(t,n,v)
end
这时为了测试一个变量是否存在,就不能简单地将它与nil比较。因为如果它为nil,访问就会抛出一个错误。
这时可以使用rawget来绕过元方法:
if rawget(_G, var) == nil then
<var 没有声明>
end
正如前面提到的,不允许全局变量具有nil值,因为具有nil值得全局变量都会自动地认为是未声明的。要就纠
正这个问题并不难,只需引入一个辅助的table用于保存已声明变量的名称。一旦调用了元方法,元方法就检
查这个table,以确定变量是否已声明过,代码如下:
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 toundeclared variable"..n, 2)
end
declaredNames[n] = true
end
end,
__index = function(_, n)
if not declaredNames[n] then
error("attemptto read undeclaredvariable"..n, 2)
else
return nil
end
end,
})
此时,即使是x=nil这样的赋值也可以起到声明全局变量的作用。
上述两种声明全局变量的方法所导致的开销可以忽略不计。第一种方法中,完全没有涉及到元方法的调用。
第二种方法虽然会调用到元方法,但只有当程序访问一个为nil的变量时才会发生。
14.3 非全局的环境
关于环境的一大问题在于它是全局的,任何对它的修改都会影响程序的所有部分。例如,若安装一个元表用
于控制全局变量的访问,那么整个程序都必须遵循这个规范。当使用某个库时,没有声明就使用了全局变量,
那么这个程序就无法运行。Lua5.1对这个问题进行了改进,它允许每个函数都拥有一个自己的环境来查找全局变量。
可以通过函数setfenv来改变一个函数的环境。这个函数的参数是一个函数和一个新的环境table。第一个参数
处理可以指定为函数本身,还可以指定为一个数字,以表示当前函数调用栈中的层数。数字1表示当前函数,
数字2表示调用当前函数的函数,以此类推。
一旦改变了环境,所有的全局访问都会使用新的table。如果新table是空的,那么就会丢失所有的全局变量,
包括_G.所以应该先将一些有用的值录入其中,例如原来的环境:
a = 1 -- 创建一个全局变量
setfenv(1, {g=_G}) --改变当前环境
g.print(a) -->nil
g.print(g.a) -->1
另一种组装新环境的方法是使用继承:
a = 1
local newgt = {} --创建新环境
setmetatable(newgt,{__index = _G})
setfenv(1, newgt) --设置它
print(a) --> 1
这段代码,新环境从原环境中继承了print 和 a。然而,任何赋值都发生在新的table中。
若误改了一个全局变量也没什么,仍然能通过_G来修改原来的全局变量:
-- 继续前面的代码
a = 10
print(a) -->10
print(_G.a) -->1
_G.a = 20
print(_G.a) -->20
每个函数及某些closure都有一个继承的环境。下面这段代码就演示了这种机制:
function factory()
return function()
return a -- 全局的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
factory函数创建了一个简单的closure,这个closure返回了它的全局a的值。每次调用factory都会创建一个
新的closure和一个属于该closure的环境。每个新创建的函数都继承了创建它的函数的环境。因此上例中
的closure都共享一个全局环境。这个环境中a为3,当调用setfevn(f1,{a=10})时,就改变了f1的环境,在新
环境中a为10.这期间f2的环境并未受到影响。
由于函数继承了创建其函数的环境。所以一个程序块若改变了它自己的环境,那么后续由它创建的函数都
将共享这个新环境。