Lua全局变量

使用动态名字访问全局变量

通常,赋值操作对于访问和修改全局变量已经足够。然而,我们经常需要一些原编程(meta-programming)的方式,比如当我们需要操纵一个名字被存储在另一个变量中的全局变量,或者需要在运行时才能知道的全局变量。为了获取这种全局变量的值,有的程序员可能写出下面类似的代码:

 loadstring("value = " .. varname)() 
or 
 value = loadstring("return " .. varname)() 

如果 varname 是 x,上面连接操作的结果为:“return x”(第一种形式为 “value = x”),当运行时才会产生最终的结果。

然而这段代码涉及到一个新的 chunk 的创建和编译以及其他很多额外的问题。你可以换种方式更高效更简洁的完成同样的功能,代码如下:
value = _G[varname]
因为环境是一个普通的表,所以你可以使用你需要获取的变量(变量名)索引表即可。

也可以用相似的方式对一个全局变量赋值:_G[varname] = value。小心:一些程序员对这些函数很兴奋,并且可能写出这样的代码:_G[“a”] = _G[“var1”],这只是 a = var1的复杂的写法而已。

对前面的问题概括一下,表域可以是型如"io.read" or "a.b.c.d"的动态名字。我们用循环解决这个问题,从_G 开始,一个域一个域的遍历:

function getfield (f) 
local v = _G -- start with the table of globals 
for w in string.gfind(f, "[%w_]+") do
 v = v[w] 
end 
return v 
end 

我们使用 string 库的 gfind 函数来迭代 f 中的所有单词(单词指一个或多个子母下划线的序列)。相对应的,设置一个域的函数稍微复杂些。赋值如:

a.b.c.d.e = v 
实际等价于: 
local temp = a.b.c.d 
temp.e = v 

也就是说,我们必须记住最后一个名字,必须独立的处理最后一个域。新的 setfield函数当其中的域(译者注:中间的域肯定是表)不存在的时候还需要创建中间表。

function setfield (f, v) 
local t = _G -- start with the table of globals 
for w, d in string.gfind(f, "([%w_]+)(.?)") do
 if d == "." then -- not last field? 
 t[w] = t[w] or {} -- create table if absent 
 t = t[w] -- get the table 
 else -- last field 
 t[w] = v -- do the assignment 
 end 
end 
end 

这个新的模式匹配以变量 w 加上一个可选的点(保存在变量 d 中)的域。如果一个域名后面不允许跟上点,表明它是最后一个名字。(我们将在第 20 章讨论模式匹配问题)。使用上面的函数

setfield("t.x.y", 10) 
创建一个全局变量表 t,另一个表 t.x,并且对 t.x.y 赋值为 10print(t.x.y) --> 10 
print(getfield("t.x.y")) --> 10 

声明全局变量

全局变量不需要声明,虽然这对一些小程序来说很方便,但程序很大时,一个简单的拼写错误可能引起 bug 并且很难发现。然而,如果我们喜欢,我们可以改变这种行为。因为 Lua 所有的全局变量都保存在一个普通的表中,我们可以使用 metatables 来改变访问全局变量的行为。第一个方法如下:

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, 
}) 
这样一来,任何企图访问一个不存在的全局变量的操作都会引起错误:
> a = 1 
stdin:1: attempt to write to undeclared variable a 
但是我们如何声明一个新的变量呢?使用 rawset,可以绕过 metamethod:
function declare (name, initval) 
 rawset(_G, name, initval or false) 
end 
or 带有 false 是为了保证新的全局变量不会为 nil。注意:你应该在安装访问控制以前(before installing the access control)定义这个函数,否则将得到错误信息:毕竟你是在企图创建一个新的全局声明。只要刚才那个函数在正确的地方,你就可以控制你的全局变量了:
> a = 1 
stdin:1: attempt to write to undeclared variable a 
> declare "a"
> a = 1 -- OK 
但是现在,为了测试一个变量是否存在,我们不能简单的比较他是否为 nil。如果他是 nil 访问将抛出错误。所以,我们使用 rawget 绕过 metamethod:
if rawget(_G, var) == nil then
-- 'var' is undeclared 
 ... 
end 
改变控制允许全局变量可以为 nil 也不难,所有我们需要的是创建一个辅助表用来保存所有已经声明的变量的名字。不管什么时候 metamethod 被调用的时候,他会检查这张辅助表看变量是否已经存在。代码如下:
local declaredNames = {} 
function declare (name, initval) 
 rawset(_G, name, initval) 
 declaredNames[name] = true
end 
setmetatable(_G, { 
 __newindex = function (t, n, v) 
if not declaredNames[n] then
 error("attempt to write to undeclared var. "..n, 2) 
else 
 rawset(t, n, v) -- do the actual set 
end 
end, 
 __index = function (_, n) 
if not declaredNames[n] then
 error("attempt to read undeclared var. "..n, 2) 
else 
 return nil
end 
end, 
}) 
两种实现方式,代价都很小可以忽略不计的。第一种解决方法:metamethods 在平常操作中不会被调用。第二种解决方法:他们可能被调用,不过当且仅当访问一个值为nil 的变量时。
发布了290 篇原创文章 · 获赞 153 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39885372/article/details/104386412