- Lua是由宿主语言调用的,因此“程序”的概念不明确,导致全局变量在逻辑上难以控制全局的范围
Lua为了解决该问题,不使用全局变量,而是对全局变量进行模拟
全局环境table _G
Lua将所有全局变量保存在全局环境(global environment)的普通table中(该table为_G)
- 优点:简化Lua内部实现,可以使用table操作修改这个table
- Lua将全局环境资自身也保存在全局变量_G中(_G._G与_G等价)
--遍历全局环境中的所有变量
for k, v in pairs(_G) do
print(k, v)
end
使用动态名称的全局变量
- 又称为元编程:操作全局变量时,全局变量的名称储存在另一变量中,或需要由计算得到
- 如:value=load(“return”..varname)()
- 如果varname是x,就相当于return x,但是涉及了代码创建和编译,开销大
- 使用value=_G[varname],效率高出一个数量级
- 如:value=load(“return”..varname)()
- 使用_G[varname] = value :为动态计算出的全局变量赋值
```lua
—这些代码解决了不能输出G[“A.B.c”]这类的问题
function getfield(f)
local v = _G —从全局变量table _G开始迭代
for k in string.gmatch(f, “[%a][%w_]*”) do
end return v endv = v[k]
function setfield(f, v) local t = G for v1, v2 in string.gmatch(f, “([%a][%2_]*)(%.?)”) do if v2 == “.” then —查看是不是要访问的最后一个 t[v1] = t[v1] or {} —该table不存在,则创建 t = t[v1] else t[v1] = v —赋值 end end end
<a name="uZpsz"></a>
### 全局变量的声明、检查
<a name="FW2NQ"></a>
#### 检测不存在的全局变量
- 方法1
- 通过**对**全局环境**table _G设置元表**,通过元表来**发现不存在的全局变量**的情况
- 使用**rawset**和**rawget**函数来**绕过元方法**进行全局变量操作
```lua
setmetatable(
_G,
{
__newindex = function(t, k, v) --元方法:处理新建不存在的全局变量
error("attempt to write to undeclared variable" .. k)
end,
__index = function(t, k) --元方法:处理访问不存在的全局变量
error("attempt to read undeclared variable " .. k)
end
}
)
function declare(name, initval)
rawset(_G, name,initval or false) --绕过元方法进行赋值,
--使用initval or false来处理赋nil值的情况(模拟创建全局变量而不复制的情况)
end
方法2
不能直接与nil进行比较,因为元方法中会对其直接报错
使用一个辅助table,保存已声明的全局变量,调用元方法就会检查该table
local declaredName = {} --用于保存已声明变量的辅助table setmetatable( _G, { __newindex = function(t, k, v) if not declaredName[k] then --在辅助table中存在则跳过,不存在则执行 local w = debug.getinfo(2, "S") if w ~= "main" and w ~= "C" then error("attempt to write undeclared varible" .. k) end declaredName[n] = true --在辅助table中加入该键 end rawset(t, k, v) --填过元表插入table end, __index = function(t, k) if not declaredName[k] then --在辅助table中存在则跳过,不存在则执行 error("attempt to read undeclared varible" .. k) else return nil end end } )
strict.lua(重要)
注:上述的多种方法造成的开销基本都可以忽略不计,最好直接调用Lua发行的strict.lua模块
- 一种在普通table操作期间不会执行
- 一种只有在访问值为nil的变量时执行
Lua中全局环境的解读(适用于Lua5.2)
- Lua中实际上并没有全局变量,全局变量只是编译器对其的一种模拟机制
- 自由名称:没有显式声明上的名称,即不出现在对应局部变量的范围内
- 如:local z = 10 ; x = y + z ;中x和y是自由名称,而z不是
- Lua将x转换为_ENV.x使代码转换为_ENV.x = _ENV.y + z
- 如:local z = 10 ; x = y + z ;中x和y是自由名称,而z不是
- _ENV:一个预定义的上值,Lua是在该值存在的情况下编译的代码。
- 一个外部的局部变量
- 外部:相对于_ENV来说,可以解释为传递给_ENV的参数。如:_ENV=var;var则为_ENV的外部变量
- 初始值可以是任意table
- 加载时,load会使用预定义的上值,进行初始化全局环境
- 一个外部的局部变量
--可以将代码的处理流程简单的转换为如下形式 local _ENV = the_global_environment(全局环境) --预定义的上值 return function(...) local z = 10 _ENV.x = _ENV.y + z end
Lua处理全局变量的方式:
- 编译所有代码前,外层创建全局变量_ENV
- 所有自由名称转换为_ENV.var
- 函数load,使用全局环境初始化代码段的第一个上值,Lua语言内部维护一个table
使用环境:_ENV
- _ENV只是一个普通的变量,但是 _ENV = nil 会使得后续代码不能直接访问全局变量
- 可以使用_ENV来绕过局部声明(_G也可以做到)
- 通常_G和_ENV指的是同一个table
- 对自由名称的使用会绑定在当前环境中 ```lua local print, sin = print, math.sin _ENV = nil —更改新的环境 print(“hello”) —输出:hello,已保存在局部变量中的可以访问 print(sin(10)) —输出:-0.54402111088937 print(math.cos(10)) —报错,全局变量已经不能再访问
a = 1 —添加至全局变量 local a = 10 —添加至局部变量 print(a) —输出:10,局部变量中的值 print(_ENV.a) —输出:1 ,全局变量中的值
<a name="vx850"></a>
### _ENV VS _G
- _ENV是一个**局部变量**,所有对**“全局变量”**的访问实际上**访问_ENV**
- 该全局变量指:**当前环境中的全局变量**
- _ENV永远**指向当前环境**
- **修改_ENV**即可**修改当前环境**,修改**之后**,**局部变量**就会**保存在新的_ENV环境**中
- **修改**了_ENV环境之**后**,**全局变量**的**访问**就会使用**新的_ENV table**
- **新环境**_ENV table中**没有任何全局变量**,包括函数;
- 在使用新_ENV之前,**应该将有用的值放入新环境中**(如**全局函数_G**)
- _G是一个任何情况下都没有任何特殊状态的“全局变量”
- 没有改变其值时,**_G**通常指向的是Lua**最初环境**中的**全局环境**
- 只有在**初始化全局table时**,会自动让**_G指向自己**,这是才会出现特殊状态(即**_G就是_ENV**)
```lua
a = 15 --在初始环境的全局变量中声明a
_ENV = {_G = _G} --新建一个环境,将初始环境中的全局变量加入新环境中
a = 1 --此时的全局变量a储存在,新环境的_ENV中
_G.print(a) --输出:1 -_G.print实际上被编译为_ENV.G.print;a被编译为_ENV.a
_G.print(_G.a) --输出:15 -_G.a被编译为_ENV._G.a
_ENV = {_G = _G} --再新建一个新环境_ENV,最右边的_G被编译为_ENV._G
a = 25 --储存在新的环境中
_G.print(a) --输出:25
_G.print(_G.a) --输出:15
--将初始全局变量给新环境之后,_ENV中的_G指代的就是初始环境中的全局变量,
--而_ENV中的是当前环境中的全局变量
继承方式创建新环境
- 新函数会继承最初环境的全局变量中的函数,
新环境中的赋值都会保存到新table中(_ENV)
a = 1 --保存到全局变量中 local newgt = {} setmetatable(newgt, {__index = _G}) --给新环境设置元表 _ENV = newgt --设置新环境 print(a) --输出最初环境中的a,并且能够使用原本全局变量中的函数
使用私有环境定义函数
可以做到让多个函数共享一个公共环境
- 还可以让一个函数使用多个环境
```lua
function factort(_ENV) —使用闭包保存环境
return function() —返回一个匿名函数
end endreturn a
f1 = factort({a = 1}) —a储存在闭包中,调用f1()则可访问环境中的a f2 = factort({a = 2}) print(f1()) —输出:1 print(f2()) —输出:2
<a name="tiLcl"></a>
### 环境和模块
- **模块的特点**:在**模块中修改环境(_ENV),不会导致调用该模块的Lua程序环境(_ENV)改变**
- 依据此特点:
- 修改模块的环境,该模块的所有共有函数都会进入该环境中
- **解决**了**模块**中忘记使用local导致**污染全局环境**的问题
- **注:使用原始的基本方法,会使得代码更加清晰**
- **可以在模块中使_ENV=nil,这样如果在模块中对全局变量赋值,就会抛出异常**
- **先将全局变量_G保存为局部变量 或 将要访问的模块保存为局部变量**
```lua
---使用模块中环境的特点来创建模块
--test2.lua
local M = {} --创建该模块使用的环境
_ENV = M --替换环境
a = 1 --虽然a是全局变量,但是是保存在环境M中的全局变量
function add(c1, c2) --保存在环境M中的全局函数
return c1 + c2
end
return M
--test.lua
local M = require("test2") --调用模块test2.lua
print(a) --输出:nil 因为模块的全局变量a并不在此时全局环境中
print(M.add(2, 3)) --输出:5
---使用_ENV=nil,让模块无法改变全局变量
local _G = _G
_ENV = nil --清空环境,只能操作局部变量
local a=1 --正确,因为a为局部变量
b = 1 --会报错,因为这样给全局变量a赋值了
--------------
--更规范的方法:将要用的模块保存
local print=print --只将要用的模块保存
local io=io
_ENV=nil --清空环境,只能操作局部变量
Lua按顺序编译,调用时运行
local foo --设置为局部
do
local _ENV = _ENV --将do-end内的局部_ENV设置为全局的_ENV
--因为_ENV为table,Lua中为table类型的标识符可以看成指针,指向同一内存地址,并不是复制
function foo()
print(X) --编译为_ENV.print(_ENV.X),这里的_ENV为do-end的局部_ENV
end
end
X = 13 --保存在初始环境的全局变量中
_ENV = {X=11} --更改环境变量
foo()
--运行函数foo()中已编译的代码,即_ENV.print(_ENV.X)的编译结果,
--但是该_ENV指的是do-end局部变量中的,并不是当前环境变量中的(不是同一指针)