第十四课 环境

Lua将其所有的全局变量保存在一个常规的table中,这个table称为“环境”。Lua将环境table自身保存在一个全局变量_G中。
for n in pairs(_G) do print(n) end --打印当前环境中所有全局变量的名称

具有动态名字的全局变量
对于访问和设置全局变量,通常赋值操作就可以了。不过有时也会用到一些元编程的形式。例如,当操作一个全局变量时,而它的名称却存储在另一个变量中,或者需要通过运行时的计算才能得到。为了获取这个变量的值,许多程序员都试图写出这样的代码:
abc = 10
varname = "abc"
cd = loadstring("return " .. varname)()
print(cd) -->10
然而,在这段代码中包含了新程序块的创建和编译。因此可以 使用下面的代码来完成相同的效果,但是效率上却比上例中高出一个数量级:
cd= _G[varname ]
print(cd) -->10
正因为环境是一个常规的table,才可以使用一个key(变量名)去直接索引它。类似地,还可以动态地计算出一个名称,然后将一个值 赋予具有该名称的全局变量:
_G[varname] = 22
print(abc) -->22
上面问题的一般形式是,允许使用动态的字段名,如“io.read”或者“a.b.c.d”。如果直接写_G["io.read"]则不会从table io中得到该字段read。但可以写一个函数getfield来实现这个效果。即通过调用getfield("io.read")返回所要求的结果。这个函数是一个主循环,从_G开始 逐个字段地深入求值:
function getfield (f)
local v = _G
for w in string.gmatch(f, "[%w_]+") do
v = v[w]
end
return v
end
与之对应的设置字段的函数则稍微显得复杂。像a.b.c.d=v这样的赋值等价于以下代码:
local temp = a.b.c
temp.d = v
也就是说,必须检索到最后一个名称,然后分别进行操作。
function setfield (f, v)
local t = _G
for w, d in string.gmatch(f, "([%w_+](%.?)") do
if d == "." then
t[w] = t[w] or {}
t = t[w]
else
t[w] = v
end
end
end

全局变量声明
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
另一种更简单的方法就是只允许在主程序块中对全局变量进行赋值,那么当声明以下变量时:
a = 1
就只需要检查此赋值是否在主程序块中。这可以使用debug库,调用debug.getinfo(2, "S")将返回一个table,其中的字段what表示了调用元方法的函数是主程序块还是普通的Lua函数,又或是C函数。可以通过该函数将--newindex元方法重写:
__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
为了测试一个变量是否存在,就不能简单地将它与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 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代码时使用它,可以养成良好的习惯。

非全局的环境
关于“环境”的一大问题在于它是全局的,任何对它的修改都会影响程序的所有部分。例如,若安装一个元表用于控制全局变量的访问,那么整个程序都必须 遵循这个规范。当使用某个库时,没有先声明就使用了全局变量,那么这个程序就无法运行了。
Lua 5对于这个问题进行了改进,它允许每个函数拥有一个自己的环境来查找全局变量。可以通过函数setfenv来改变一个函数的环境。该函数的参数是一个函数和一个新的环境table。第一个参数除了可以指定为函数本身,还可以指定为一个数字,以表示当前函数调用栈中的层数。数字1表示当前函数,数字2表示调用当前函数的函数,依次类推。
第一次天真地试用setfenv可能会带来糟糕的结果。代码如下:
a = 1
setfenv(1, {})
print(a)
会导致:
stdin:5:attempt to call global 'print' (a nil value)
一旦改变了环境,所有的全局访问就会使用新的环境table。如果新的table是空的,那么就会丢失所有的全局变量,包括_G。所以应该先将一些有用的值录入其中,例如原来 的环境:
a = 1
setfenv(1, {g = _G})
g.print(g.a) -->1
g.print(a) -->nil
此时访问“全局的”g就会得到原来的环境,这个环境中包含了字段print。
可以使用名字_G来替代g,从而重写前例:
setfenv(1, {_G = _G})
_G.print(a) -->nil
_G.print(_G.a) -->1
对于Lua来说,_G只是一个普通的名字。当Lua创建最初的全局table时,只是将这个table赋予了全局变量_G,Lua不会在意这个变量_G的当前值。setfenv不会在新环境中设置这个变量。但如果希望在新环境中引用最初的全局table,一般使用_G这个名称即可,如上例。
另一种组装新环境的方法是使用继承:
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,当调用setfenv(f1, {a = 10})时,就改变了f1的环境,在新环境中a为10。这期间f2的环境并未受到影响。
由于函数继承了创建其函数的环境。所以一个程序块若改变了它自己的环境,那么后续由它创建的函数都将共享这个新环境。这项机制对于创建名称空间是很有用的。






















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值