快速掌握Lua 5.3 —— 环境

Q:_ENV

A:_ENV是一个普通的”table”,它其中存储了当前运行环境中所有的全局变量。

type(_ENV)    --> table
for n in pairs(_ENV) do print(n) end    -- 打印当前运行环境中所有全局变量。

我们平常在程序中所写的全局变量就存储在其中,全局变量名是”key”,全局变量值是”value”,

a = 10
print(_ENV["a"])    --> 10

Q:如何更改运行环境?

A:全局变量的问题也是他是全局的,任何对其的修改都会影响到所有的程序。举个例子,当你规定全局变量的使用必须声明时,所有的程序就都需要遵循这一规则。如果你想使用一个库,那么如果你不声明的话就会报错。
通过更改_ENV,可以更改当前程序的运行环境,

--[[ 请在文件中运行上述例子,如果在交互模式中运行,请用"do-end"包裹起来。
     因为在交互模式中运行的每一行代码都是一个单独的"chunk",
     所以"_ENV = {}"只对它自己的那一行"chunk"起作用。]]
a = 1    -- create a global variable
_ENV = {}    -- change current environment to a new empty table
print(a)    --> attempt to call a nil value (global 'print')

print()也是一个全局函数,因为修改了环境变量,原先的print()_G中,而修改后的环境变量集合中没有print(),所以会报错。我们可以通过保存原先的运行环境,从而使用原先的环境变量,

a = 1    -- create a global variable -- "a"存储在"_G"中,"_G.a"。
_ENV = {_G = _G}    -- 新的环境变量中"_G"域存储原先的"_G"。
_G.print(a)    --> nil -- "a""_G"中,不在当前的环境变量中。
_G.print(_G.a)    --> 1

也可以使用”metatable”继承的方式实现,

a = 1    -- create a global variable
local newgt = {}    -- create new environment
-- 新的环境变量找不到的"key",去原先的"_G"中寻找。
setmetatable(newgt, {__index = _G})
_ENV = newgt
print(a)    --> 1
-- 如果你在重定义环境变量集合之前定义过全局变量,那么现在你也不用担心更改与之前同名的全局变量会造成问题。
-- continuing previous code
a = 10
print(a)      --> 10
print(_G.a)   --> 1
_G.a = 20
print(_G.a)   --> 20

关于”_ENV”与”_G”的关系,我理解的还不够透彻。下面引用的这段资料讲的可能会更详细些。
原文连接:【lua5.2技术干货】带你理解_ENV和_G, 不懂的同学来学习吧~

5.1之前, 全局变量存储在_G这个table中, 这样的操作:
a = 1
相当于:
_G[‘a’] = 1

但在5.2之后, 引入了_ENV叫做环境,与_G全局变量表产生了一些混淆,需要从原理上做一个理解。
在5.2中,
操作a = 1
相当于
_ENV[‘a’] = 1
这是一个最基础的认知改变,其次要格外注意_ENV不是全局变量,而是一个upvalue(非局部变量)。

其次,_ENV[‘_G’]指向了_ENV自身,这一目的是为了兼容5.1之前的版本,因为之前你也许会用到:

_G[‘a’] = 2 , 在5.2中, 这相当于_ENV[‘_G’][‘a’],为了避免5.1之前的老代码在5.2中运行错误,所以5.2设置了_ENV[‘_G’]=_ENV来兼容这个问题。然而你不要忘记_ENV[‘_G’]=_ENV,所以一切都顺理成章了。

在5.1中,我们可以为一段代码块(或者函数)设置环境,使用函数setfuncs,这样会导致那一段代码/函数访问全局变量的时候使用了setfuncs指定的table,而不是全局的_G。

在5.2中,setfuncs遭到了废弃,因为引入了_ENV。 通过在函数定义前覆盖_ENV变量即可为函数定义设置一个全新的环境,比如:
a = 3
function get_echo()
local _ENV={print=print, a = 2}
return function echo()
print(a)
end
end

get_echo()()
会打印2,而不是3,因为echo函数的环境被修改为{print=print, a=2},而print(a)相当于访问_ENV[‘a’](先忘掉那为了兼容而存在的_G)。

这就是_ENV的基本用法了。

另外,不得不提到lua的C支持中关于全局变量与环境的细节,只能简单描述,你必须自己试试才能记得清楚。

lua_setglobal/lua_getglobal都是操作lua_State注册表中LUA_RIDX_GLOBALS伪索引指向的全局变量表,与lua中访问_ENV[‘a’]或者a是不同的。

lua_load加载lua代码后会返回一个函数,默认会给这个函数设置一个upvalue就叫_ENV,起值是LUA_RIDX_GLOBALS的全局变量表,你可以lua_setupvalue设置这个函数的upvalue,即下标1的upvalue,因为这个位置是这个函数的_ENV表存放位置(你可以通过lua_setupvalue的返回值印证这一点)

这里巧妙的是,lua_State会在创建时保证LUA_RIDX_GLOBALS的全局变量表中包含一个指向自己的_G元素,这样就保证了在不调用lua_setupvalue的情况下该返回函数的_ENV[‘_G’]是指向自己的,即LUA_RIDX_GLOBALS这个全局表。(其实你的lua解释器就是简单的lua_load后pcall的,对于一个刚启动lua_State来说是没有_ENV的,是lua解释器load你的代码时自动给带上的_ENV,其值是lua_state的LUA_RIDX_GLOBALS全局表。)

一些有意思的东西是需要你自己摸索的,lua语言自身就很简练,并且所有东西都不是什么神秘的事情,可以通过读源码或者试验摸索得到。

最后,提一下,lua_state启动后在注册表里LUA_RIDX_GLOBALS下标存放的全局表一定有一个元素是指向自己的,即_G.

附加:

1、_ENV中存储着它本身,

_ENV == _ENV._G    --> true
_ENV == _ENV["_G"]    --> true

2、为一个不存在的”table”中的元素赋值会报错,

print(t)    --> nil
t.x.y = 10    --> attempt to index a nil value (global 't')

但是通过操作_ENV,我们可以解决这一问题,

function getfield (f)
    local v = _ENV    -- start with the table of globals
    --[[ 匹配模式中未指定捕获,"string.gmatch()"会返回匹配的值,
         即"t.x.y"会匹配3次,每次返回"t"、"x"、"y"。]]
    for w in string.gmatch(f, "[%w_]+") do
        v = v[w]    -- get the next table or get the final value.
    end
    return v
end

function setfield (f, v)
    local t = _ENV    -- start with the table of globals
    --[[ 匹配模式中指定了捕获,"string.gmatch()"依次返回捕获的值,
         即"t.x.y"会匹配3次,每次返回"t"和"."、"x"和"."、"y"和"nil"。]]
    for w, d in string.gmatch(f, "([%w_]+)(.?)") do
        if d == "." then    -- 有"."说明没有到最后,到最后时是"nil"。
            t[w] = t[w] or {}    -- 如果此"table"不存在,则创建。
            t = t[w]    -- get the table
        else    -- 为"nil",到了最后一个域。
            t[w] = v    -- do the assignment
        end
    end
end

print(t)    --> nil
setfield("t.x.y", 10)
print(t.x.y)    --> 10
print(getfield("t.x.y"))    --> 10

3、Lua中的全局变量不需要声明。虽然这对一些小程序来说很方便,但程序很大时,一个简单的拼写错误可能引起”bug”并且很难发现。然而,如果我们喜欢,可以使用”metatable”来改变这种行为,

--[[ 此函数一定要写在下面的"setmetatable()"前面,
     因为这里的"declare()"也是个全局函数,写在其后依旧会报错。]]
function declare (name, initval)
    --[[ "_ENV"中未声明的全局变量的默认值都是"nil",
         为了与已声明却未给初始值的全局变量的默认值区别开,后者的默认值是"false"。]]
    rawset(_ENV, name, initval or false)
end

setmetatable(_ENV, {
    __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 = 7    --> attempt to write to undeclared variable a
declare("a")
print(a)    --> false
a = 7
print(a)    --> 7

-- 此种方式下最好不要让"a = nil",因为一旦这样,已声明的变量"a"在"_ENV"中就变成了未声明的变量,再次使用时就会报错。
a = nil
a = 1 --> attempt to write to undeclared variable a

现在,如果想测试一个变量是否存在,不能简单的将这个变量与”nil”比较,需要使用rawget()

if(x == nil) then    --> attempt to read undeclared variable x
    print("Variable does not exist.")
end

if(rawget(_ENV, x) == nil) then
    print("Variable does not exist.")    --> Variable does not exist.
end

如果希望变量在使用前需要声明,同时又希望未给默认值的已声明变量的默认值是”nil”,那么可以使用一个辅助表保存已声明的变量,

local declaredNames = {}

function declare (name, initval)
    rawset(_ENV, name, initval)
    declaredNames[name] = true
end

setmetatable(_ENV, {
    __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,
})

a = 7    --> attempt to write to undeclared var. "a"
declare("a")
print(a)    --> nil
a = 7
print(a)    --> 7

4、当你创建一个新的函数时,他从创建他的函数继承了环境变量。所以,如果一个”chunk”改变了他自己的环境,这个”chunk”所有在改变之后定义的函数都共享相同的环境,都会受到影响,这对创建命名空间是非常有用的机制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值