一、前言
Lua 是动态语言,无法区分常量和变量。
二、_G
Lua 将全局变量保存在一个全局环境的表中 ,而这个表就是 _G ,因为 _G 是一个表所以可以像其他表一样操作。
值得注意的是,_G._G
指向自身 _G
,即 _G._G == _G
,这样才能在代码段中正常的使用 _G
变量(下面的章节会分享为什么)
打印 _G
和 _G._G
便可印证这一点
lua
print(_G, _G._G) --> table: 0x600001ff0200 table: 0x600001ff0200
我们设置的所有的全局变量,如果没有其他的特殊处理(后面小节会分享有什么特殊处理),都会最终存储在全局变量 _G 中。
lua
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
lua
local i = 10
j = 100
k = j + i
print(i, j, k)
会被转换为
lua
local i = 10
_ENV.j = 100
_ENV.k = _ENV.j + i
print(i, _ENV.j, _ENV.k)
对于上面的代码段, _ENV 的存在点,可以理解为如下
lua
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 两者其实都是同一个表
lua
do
print(_ENV, _G) --> table: 0x6000007d4200 table: 0x6000007d4200
end
1-1、为什么要 _G._G = _G
前面提及到 _G 表中有一个 _G 字段指向自身,即 _G._G = _G
,这是为了能达到以下效果
lua
_G.name = "jiang peng yong"
这段代码就会被编译器转换为
lua
_ENV._G.name = "jiang peng yong"
而前面讲到,在不修改 _ENV 和 _G 的时候,两者是一样的,所以可以理解为以下代码
lua
_G._G.name = "jiang peng yong"
如果没有 _G._G = _G
,则这种获取 _G
的使用方式就无法达到了。
2、_ENV 的使用
从上一小节可以知道,_ENV 也只是一个外部局部变量,并没有其他的特殊的,而且编译器为全局变量增加 _ENV 也只是单纯的增加,没有特别的限制和副作用。
所以我们可以对 _ENV 进行设置,达到不同的玩法
2-1、去除上值,隔绝与全局的关系
可以通过将 _ENV
设置为 nil
,从而打断了与全局关系
lua
-- 通过将 _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 小节中已解释。
lua
_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
。
lua
-- 全局设置一个变量
_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 的值不同
lua
name = "jiang"
do
-- 注意这里的 _ENV 是局部的
local _ENV = { _G = _G, name = "江" }
_G.print(name) --> 江
end
print(name) --> jiang
例子二: 给方法传递 _ENV ,这样也可以达到方法内的 _ENV 被改变
lua
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 则是一个在任何情况下都没有任何特殊状态的全局变量。
五、模块
在定义模块的时候,更多的应该不污染全局环境,保证好模块内部的变量均是局部的,最后返回模块的值给到外部使用即可。
想要在代码层面约束,而不只是单纯的人为约定(编码过程中都会疏忽和遗漏),可以在模块内部的最开始就加入
lua
local innerENV = {}
local _G = _G
_ENV = innerENV
这样模块内部的 "全局变量" 就存储在了 innerENV 中,而不会污染到 _G ,而模块内部需要真正的全局变量时,则使用 _G 进行访问就可以了。
六、load 和 loadfile 的上值
在前面的文章中,有分享到 load 函数和 loadfile 函数可以传递 env (上值),而参数的 env 就是指被编译加载代码段的 _ENV 。如果不设置该值,就会使用加载该代码段的 _ENV 作为默认值传递进内部。
如果 load 或 loadfile 外部传入 env ,且不给访问外部全局变量(即无法访问到 _G ),则会形成一个类似沙盒的模式,被加载的代码段是无法改变到外部的环境,达到保护主代码的作用。
1、loadfile 上值
lua
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
lua
width = 1000
height = 1000
print(name) --> nil (因为上面的代码没有将 _G 传入,获取不到外部的 name )
name = "将 name 进行篡改 via loadfile"
2、load 上值
lua
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 加载的上值
lua
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
修改函数的上值
lua
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吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。