定义在所有函数外部的变量我们可以称之为全局变量(Global Variable
),它的作用域默认是整个程序。但Lua作为一种嵌入式语言,代码段(chunk
)都是由宿主应用调用的,它自身都不知道会被嵌入到哪个应用程序中。为了解决这个问题,它并没有使用全局变量,而是通过table
对全局变量进行模拟。我们可以认为Lua语言把所有的全局变量保存在一个称为全局环境(Global Environment
)的普通表中。
全局环境表 _G
Lua语言将全局变量都保存在_G表中,这样即简化了它的内部实现,也可以让用户像操作其他表一样使用这个表。既然是一个表,那我们就可以打印出它的所有内容。
for n in pairs(_G)
do
print(n) --打印出_G表中的所有变量
end
_G中除了自定义的全局变量,还包括许多Lua预定义的函数,比如print
、math
、io
等。
我们还可以通过_G
来快速访问全局变量,例如访问一个全局变量a,并尝试访问局部变量b。
a = 123
local b = 456
print(_G.a) --123,等价于 print(_G["a"])
print(_G.b) --nil,局部变量并没有保存在_G中
对于局部变量,既不会被保存在 _G 中,也不会保存在 _Env 中。
_ENV表
对于一个没有进行显示声明的变量,Lua是怎么处理的呢?
local z = 10
x = y + z
lua把所有代码段都当作匿名函数来处理,并且把代码段中未显式声明的变量xx转换为_ENV.xx
,因此上述代码会被编译成如下形式
local z = 10
_ENV.x = _ENV.y + z
_Env
是什么呢?它是lua中的一个预定义上值(upvalue
)。因为我们说过,lua中根本就不存在全局变量,为了能够让用户产生Lua中有全局变量的错觉,Lua将_Env
表被设计成一个upvalue
,所有的代码段都当作是匿名函数,所以上面的代码实际上被编译成如下的样子
local _ENV = the global environment(全局环境)
return function (...)
local z = 10
_ENV.x = _ENV.y + z
end
Lua通过使用预定义上值 _ENV
表来保存全局变量,所有对全局变量的访问都是通过_Env
引用得到,如果我们将_Env
设为nil,则后续的代码都不能直接访问全局变量,包括print
在内
a = 123
_ENV = nil
print(a)
运行上述代码会出现一个错误提示❌:attempt to index a nil value (upvalue '_ENV')
。因为代码print(a)
等价于_ENV.print(a)
,而_Env
被设置为nil
,因此会出现企图访问nil
的错误。
让我们总结一下Lua语言中处理全局变量的方式:
- 编译器在编译所有代码段前,在外层创建局部变量
_ENV
; - 编译器将所有自由名称
var
变换为_ENV.var
; - 函数
load
(或函数loadfile
)使用全局环境初始化代码段的第一个upvalue
,即Lua语言内部维护的一个普通的表。
upvalue(上值)
前面提到_Env
是一个upvalue
类型的table
, 什么是upvalue
呢?可以理解为在当前语句作用域(scope
)之上的值,一个带有upvalue
的函数,我们称之为闭包,通过一个例子直观感受一下。
local upval = 1
local upval2 = 2
function out()
local locvar = 3
print(upval)
local function inner()
print(upval+upval2+locvar)
end
inner()
end
上面例子中,out
函数外部定义了upval
和upval2
两个变量,并且内部引用了upval
变量,因此upval
是函数out
的一个upvalue
,它内部的函数inner
引用了变量upval2
,因此upval2
也是out
的上值,而inner
函数有三个上值,分别是upval
、upval2
和locvar
。
_G 和 _ENV 的关系
通常情况下,_G 和 _ENV 指向的是同一个表,但它们是两个不同的实体。 _ENV 是一个局部变量,所有对“全局变量”的访问实际上都是访问 _ENV 。 _G则是一个在任何情况下都没有任何特殊状态的全局变量。按照定义, _ENV永远指向的是当前的环境 _G在没有手动改变其值的情况下指向的是全局环境。
_ENV的主要用途是改变代码段使用的环境,一旦改变了环境,所有的全局访问就都使用新表,如果新环境是空的,那么就会丢失所有的全局变量,包括print在内。
任何一个lua函数,都至少有一个upvalue,而这个upvalue就是以 _ENV为upvalue名称的table,它默认指向了 _G。
实战一:禁止引用未声明的变量
在使用C#的时候,所有的变量都需要先声明才能够使用,而Lua并不需要,这虽然比较方便,但也容易造成难以发现的bug
。思路是为_G设置元表,并实现该元表的__index
方法,因为我们在表中引用未声明的全局变量时,会将该变量加入到 _Env表中,若该表中不存在,则会调用它的__index
方法
setmetatable(_G,{
__index = function (_,n)
error("attempt to read undeclared variable:" .. n)
end,
__newindex = function (_,n)
error("attempt to write to undeclared variable:" .. n)
end
})
现在,不论是设置还是获取未声明变量的值,都会触发error
语句。
我们可以通过rawset函数来绕过元方法,对变量进行声明。
local declare; --定义局部变量declare
function declare (name, initval)
rawset(_G, name, initval or false)
end
declare("a",123)
print(a) --123
实战二:通过_Env修改当前环境
因为_Env只是一个普通的表,所以我们能够像操作普通表一样对它进行操作。但前面说过,对全局变量的访问实际上都是访问 _Env,当 _Env被修改后,当然全局环境也就改变了。
_ENV = {} --将_Env设置为空
a = 123
print(a) --error:attempt to call a nil value (global 'print')
因为print函数也是通过_Env进行访问的,所以当 _Env被设置为空后,就无法访问print函数了。
当从外部加载一个Lua文件时,为了防止影响现有的代码,可以单独设置外部Lua文件的环境。
env = {}
loadfile("text.lua","t",env)()
这样就算加载的文件有问题,也无法对现有程序造成破坏。
_Evn也符合作用域的规则,只会在它的作用域内起作用
a = 2
do
local _ENV = {print = print,a = 14} --改变do语句块的环境(块作用域)
print(a)
end
print(a) --do语句外面的环境未更改
参考
[1] 《Lua程序设计》第四版