Lua _G表 和 _ENV表

2 篇文章 0 订阅

定义在所有函数外部的变量我们可以称之为全局变量(Global Variable),它的作用域默认是整个程序。但Lua作为一种嵌入式语言,代码段(chunk)都是由宿主应用调用的,它自身都不知道会被嵌入到哪个应用程序中。为了解决这个问题,它并没有使用全局变量,而是通过table对全局变量进行模拟。我们可以认为Lua语言把所有的全局变量保存在一个称为全局环境(Global Environment)的普通表中。

全局环境表 _G

Lua语言将全局变量都保存在_G表中,这样即简化了它的内部实现,也可以让用户像操作其他表一样使用这个表。既然是一个表,那我们就可以打印出它的所有内容。

for n in pairs(_G)
do
    print(n)    --打印出_G表中的所有变量
end

_G中除了自定义的全局变量,还包括许多Lua预定义的函数,比如printmathio等。

我们还可以通过_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函数外部定义了upvalupval2两个变量,并且内部引用了upval变量,因此upval是函数out的一个upvalue,它内部的函数inner引用了变量upval2,因此upval2也是out的上值,而inner函数有三个上值,分别是upvalupval2locvar

_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程序设计》第四版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值