Lua 环境(_G 和 _ENV)

一、前言

Lua 是动态语言,无法区分常量和变量。

二、_G

Lua 将全局变量保存在一个全局环境的表中,而这个表就是 _G ,因为 _G 是一个表所以可以像其他表一样操作

值得注意的是,_G._G 指向自身 _G,即 _G._G == _G ,这样才能在代码段中正常的使用 _G 变量(下面的章节会分享为什么)

打印 _G_G._G 便可印证这一点

print(_G, _G._G)    --> table: 0x600001ff0200	table: 0x600001ff0200

我们设置的所有的全局变量,如果没有其他的特殊处理(后面小节会分享有什么特殊处理),都会最终存储在全局变量 _G 中。

print("_G.age", _G.age)     --> _G.age	nil
age = 100
print("age", age)           --> age	100
print("_G.age", _G.age)     --> _G.age	100

三、_ENV

1、_ENV 是什么

在 Lua 中,会为每个代码段增加一个预定义上值,即 _ENV 。他是一个外部局部变量,会将代码段中使用的 “全局变量” 存在这个 _ENV 中。

Lua 编译器会将以下代码段进行转换,全局变量都会被带上 _ENV

local i = 10
j = 100
k = j + i
print(i, j, k)

会被转换为

local i = 10
_ENV.j = 100
_ENV.k = _ENV.j + i
print(i, _ENV.j, _ENV.k)

对于上面的代码段, _ENV 的存在点,可以理解为如下

local _ENV = 初始化
return function (...)
    local i = 10
    _ENV.j = 100
    _ENV.k = _ENV.j + i
    print(i, _ENV.j, _ENV.k)
end

这里可以注意到 _ENV 是一个代码段外部的局部变量 ,但是他会进行初始化,理论上他可以是任意的表,但是为了维护全局的概念,所以这里会使用 _G 进行初始化,这样就让我们无感知的使用到了全局。

我们可以将 _ENV 和 _G 进行打印,在不进行修改的情况下,_ENV 和 _G 两者其实都是同一个表

do
    print(_ENV, _G)     --> table: 0x6000007d4200	table: 0x6000007d4200
end
1-1、为什么要 _G._G = _G

前面提及到 _G 表中有一个 _G 字段指向自身,即 _G._G = _G ,这是为了能达到以下效果

_G.name = "jiang peng yong"

这段代码就会被编译器转换为

_ENV._G.name = "jiang peng yong"

而前面讲到,在不修改 _ENV 和 _G 的时候,两者是一样的,所以可以理解为以下代码

_G._G.name = "jiang peng yong"

如果没有 _G._G = _G ,则这种获取 _G 的使用方式就无法达到了。

2、_ENV 的使用

从上一小节可以知道,_ENV 也只是一个外部局部变量,并没有其他的特殊的,而且编译器为全局变量增加 _ENV 也只是单纯的增加,没有特别的限制和副作用。

所以我们可以对 _ENV 进行设置,达到不同的玩法

2-1、去除上值,隔绝与全局的关系

可以通过将 _ENV 设置为 nil ,从而打断了与全局关系

-- 通过将 _ENV 设置为 nil , 从而打断了与全局关系
local print, sin = print, math.sin
_ENV = nil
print(13)           --> 13
print(sin(13))      --> 0.42016703682664
print(math.cos(13)) --> 报错,attempt to index a nil value (upvalue '_ENV')
2-2、_ENV 设置为新的表

可以按照自己所需要的进行替换 _ENV ,进行提供一个不一样的 “上值环境” 。

例如下面的代码,就重新设置了 _ENV ,将 _G 设置为 _ENV 的字段,是因为后面的代码才可以使用过 _G 进行使用 _G 变量,具体原因在 1-1 小节中已解释。

_ENV = { _G = _G }
b = 10
_G.print("b", b)            --> b	10
_G.print("_ENV.b", _ENV.b)  --> _ENV.b	10
_G.print("_G.b", _G.b)      --> _G.b	nil

值得一提的是,Lua 约定 _G 用于指向全局变量

2-3、使用元方法设置 _ENV

可以通过设置新的 _ENV 表和对该表设置元表,进行代码段设置新的全局变量不会影响到 _G 中的值。从而达到代码段中,设置 “全局变量” 不会影响到真正的 _G

-- 全局设置一个变量
_G.name = "jiang peng yong"

-- 设置新的 _ENV 表,并且设置元表,这样 _ENV 没有的方法和变量则会在 _G 中进行获取,
-- 然后新设置的值则存储在 _ENV 中,并不会污染到 _G
local newENV = {}
setmetatable(newENV, { __index = _G })
_ENV = newENV

-- 因为 _ENV 没有 name ,所以获取的是 _G 的 name
print(name)     --> jiang peng yong
name = "江澎涌"
-- 这里获取的是 _ENV.name
print(name)     --> 江澎涌
-- 这里获取的是全局变量 name
print(_G.name)  --> jiang peng yong
2-4、局部变量 _ENV

前面有提及到, _ENV 的替换是在编译阶段进行增加,运行起来其实是没有什么特别的含义或约束,所以 _ENV 本质上是一个变量,也是符合作用域的规则。

所以我们可以进行创建局部变量 _ENV ,来达到改变局部代码块的 _ENV 值。

例子一: 可以使用局部变量,改变局部 _ENV 的值不同

name = "jiang"
do
    -- 注意这里的 _ENV 是局部的
    local _ENV = { _G = _G, name = "江" }
    _G.print(name)      --> 江
end
print(name)             --> jiang

例子二: 给方法传递 _ENV ,这样也可以达到方法内的 _ENV 被改变

do
    local i = 10
    j = 100
    k = j + i
    print(i, j, k)      --> 10	100	110
end

function factory(_ENV)
    local i = j + k
    return function()
        return i
    end
end

print(factory({ j = 10, k = 20 })())        --> 30
print(factory({ j = 100, k = 200 })())      --> 300
print(i, j, k)      --> nil	100	110

四、_G 和 _ENV 的区别

在一般情况下,_G 和 _ENV 都是指向同一个 table (从 “第三小节” 的打印能得知),因为在创建 _ENV 这一变量时,会先用 _G 对其进行初始化。

虽然他们的初始状态是一致的,但是他们有着各自的职责:

  • _ENV 是每段代码的上值(即当前的环境变量),他是一个相对于当前的运行代码块的外部局部变量。 当前运行的代码块中对 “全局变量” 的访问实际上都会转换为对 _ENV 的访问。
  • _G 则是一个在任何情况下都没有任何特殊状态的全局变量。

五、模块

在定义模块的时候,更多的应该不污染全局环境,保证好模块内部的变量均是局部的,最后返回模块的值给到外部使用即可

想要在代码层面约束,而不只是单纯的人为约定(编码过程中都会疏忽和遗漏),可以在模块内部的最开始就加入

local innerENV = {}
local _G = _G

_ENV = innerENV

这样模块内部的 “全局变量” 就存储在了 innerENV 中,而不会污染到 _G ,而模块内部需要真正的全局变量时,则使用 _G 进行访问就可以了。

六、load 和 loadfile 的上值

在前面的文章中,有分享到 load 函数和 loadfile 函数可以传递 env (上值),而参数的 env 就是指被编译加载代码段的 _ENV 。如果不设置该值,就会使用加载该代码段的 _ENV 作为默认值传递进内部。

如果 load 或 loadfile 外部传入 env ,且不给访问外部全局变量(即无法访问到 _G ),则会形成一个类似沙盒的模式,被加载的代码段是无法改变到外部的环境,达到保护主代码的作用。

1、loadfile 上值

name = "江澎涌"
local env = { print = print }
local currentPath = debug.getinfo(1, "S").source:sub(2):match("(.*/)")
loadfile(currentPath.."被加载的 config .lua", "t", env)()
print(env.width, env.height)            --> 1000	1000
print("外部全局 name :", name)         --> 外部全局 name :	江澎涌
print("加载的模块的 name :", env.name) --> 加载的模块的 name :	将 name 进行篡改 via loadfile

被加载的 config .lua

width = 1000
height = 1000

print(name)         --> nil (因为上面的代码没有将 _G 传入,获取不到外部的 name )
name = "将 name 进行篡改 via loadfile"

2、load 上值

local env = {}
f = load([[
width = 200
height = 200
name = "将 name 进行篡改 via load"
]], "load test", "t", env)
f() 
print(env.width, env.height)            --> 200	200
print("外部全局 name :", name)         --> 外部全局 name :	江澎涌
print("加载的模块的 name :", env.name) --> 加载的模块的 name :	将 name 进行篡改 via load

3、debug.setupvalue 修改方法上值

3-1、debug.setupvalue(f, up, value)
  • 第一个参数 f : 需要被设置的函数
  • 第二个参数 up :上值的索引,在这个场景中永远为 1 ,后续文章会详细阐述这一参数
  • 第三个参数 value :需要设置的新上值
3-2、如何使用

修改 load 加载的上值

age = "29"
height = 1000
local f = load("age = 20; return height;")
local env = { height = 50 }

print("未更改 f 的上值")
print(f())                      --> 1000
print(env.age, env.height)      --> nil	50
print(age, height)              --> 20	1000

print("更改 f 的上值")
debug.setupvalue(f, 1, env)
print(f())                      --> 50
print(env.age, env.height)      --> 20	50
print(age, height)              --> 20	1000

修改函数的上值

age = 29
local changeAge = function()
    print(age)
end
print("未更改 changeAge 上值")
changeAge()     --> 29

print("更改 changeAge 上值")
local env = { age = 100, print = print }
debug.setupvalue(changeAge, 1, env)
changeAge()     --> 100

七、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

你可能感兴趣的:(Lua,lua,android,开发语言,c++,c语言)