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
endget_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”所有在改变之后定义的函数都共享相同的环境,都会受到影响,这对创建命名空间是非常有用的机制。