Lua 程序设计:环境

环境

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 函数的环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值