全局变量_G有多特殊?

12

来自Lua 5.3手册摘录:

_G

一个全局变量(非函数),保存着全局环境(参见§2.2)。Lua本身不使用此变量;更改其值不会影响任何环境,反之亦然。

§2.2相关部分:

[...] 每个代码块在一个名为_ENV的外部局部变量的范围内编译,所以_ENV本身从不是代码块中的自由名称。

[...]

作为_ENV值使用的任何表都称为环境。

Lua保留了一个特殊的环境,称为全局环境。该值保存在C注册表中的一个特殊索引中。在Lua中,全局变量_G被初始化为相同的值。(_G从不在内部使用。)

当Lua加载一个代码块时,其_ENV上值的默认值是全局环境。因此,默认情况下,Lua代码中的自由名称指的是全局环境中的条目。

我理解每次加载代码块时,由于_ENV是第一个上值,所以它被“指向”全局环境表,由load_G进行指向。

> =_G, _ENV
table: 006d1bd8 table: 006d1bd8

确认两者指向同一张表。手册多次明确表示_ENV_G只是普通的名称,没有任何隐藏的含义,Lua本身也不会在内部使用它们。我尝试了下面这个代码块:

local a = { }
local b = a      -- since tables are objects, both refer to the same table object
print(a, b)      -- same address printed twice
a = { }          -- point one of them to a newly constructed table
print(a, b)      -- new, old table addresses printed

现在同样对_G_ENV进行相同的操作:

local g = _G          -- make an additional reference
print(g, _G, _ENV)    -- prints same address thrice
local p = print       -- backup print for later use
_ENV = { }            -- point _ENV to a new table/environment
p(g, _G, _ENV)        -- old, nil, new

table: 00ce1be0    table: 00ce1be0    table: 00ce1be0
table: 00ce1be0    nil                table: 00ce96e0
如果_G是一个普通的全局变量,为什么它在这里变成了nil?如果进行引用计数,当_ENV释放它时,_G仍然持有一个引用。与上面的b一样,它也应该继续保留旧表,不是吗?然而,在下面的代码块中,_G保持不变/保留!
_ENV = { _G = _G }
_G.print(_G, _ENV, _ENV._G)   -- old, new, old

但是这里被杀死了:

_ENV = { g = _G }
_ENV.g.print(_ENV, _ENV.g, _G)    -- new, old, nil

另一种情况下它是被保留的:

print(_G, _ENV)                       -- print same address twice
local newgt = {}                      -- create new environment
setmetatable(newgt, {__index = _G})   -- set metatable with _G as __index metamethod
_ENV = newgt                          -- point _ENV to newgt
print(_G, newgt, _ENV)                -- old, new, new

由于_G的行为有如此多的变化,手册所给出的最初的保证似乎不太可靠。我在这里错过了什么?


3
所有全局变量都会被自动添加前缀 _ENV.,因此在执行脚本前,任何 _G 的出现都将变成 _ENV._G - Egor Skriptunoff
非常好的观点!尽管知道这个事实,但我在这种情况下未能应用它。您能否将此评论扩展为答案?它完全解决了这个谜团。 - legends2k
1个回答

28

全局变量_G有多特殊?

它有三个特殊之处:

  1. 它使用一个名称被Lua内部保留
  2. 它是由Lua的一个标准模块(特别是"base"模块)创建的。如果您创建一个没有打开“base”模块的新的lua_State,您将没有_G变量。然而,独立解释器已经加载了所有标准库。
  3. 一些第三方Lua模块使用全局变量_G,更改/删除它可能会破坏这些模块。

_G的意义是什么?

Lua中的全局变量是使用普通表实现的。对于任何不是local变量或upvalue的变量的访问都将被重定向到该表。本地变量始终优先,因此如果您有一个全局变量和一个同名的本地变量,则始终会得到本地变量。这时就需要用到_G:如果你想要全局变量,可以使用_G.name而不是name。假设名称_G不是本地变量(它是保留给Lua的,记住吗?!),则使用表索引语法总是可以获取全局变量的值,并消除与本地变量名称的歧义。在更新的Lua版本(5.2+)中,您还可以使用_ENV.name作为替代方案,但_G是早期版本的,保持兼容性。

在其他情况下,您可能需要获取全局变量表,例如用于设置元表。Lua 允许您使用 setmetatable 函数设置元表来自定义表(和其他值)的行为,但是您必须以某种方式传递表作为参数。 _G 可帮助您完成这一操作。

如果您已经向全局变量表添加了元表,在某些情况下,您可能希望绕过刚刚安装的元方法(__index 和/或 __newindex)。您可以使用 rawgetrawset 函数,但您还需要将全局变量表作为参数传递。

请注意,上面列出的所有用例仅适用于Lua代码,而不适用于C代码。在C代码中,您没有命名的本地变量,只有堆栈索引,因此不存在歧义。如果您想要将全局表的引用传递给某个函数,则可以使用lua_pushglobaltable(它使用注册表而不是_G)。因此,用C实现的模块不使用/需要_G全局变量。这适用于Lua的标准库(也是用C实现的)。事实上,参考手册保证,Lua或其标准库不使用_G(变量,而不是表)。 < h2 > < code > _G 与< code > _ENV 有何关系?

自5.0版本以来,Lua允许您基于每个(Lua)函数的基础更改用于查找全局变量的表。在Lua 5.0和5.1中,您可以使用setfenv函数进行操作(全局表也称为“函数环境”,因此使用了setfenv这个名称)。Lua 5.2引入了一种新方法,使用另一个特殊的变量名_ENV。然而,_ENV并不是一个全局变量,Lua确保每个块都以一个_ENV upvalue开始。新方法通过让Lua将对非本地(非upvalue)变量名a的任何访问转换为_ENV.a来工作。无论_ENV在代码中的哪个位置,它都会被用于解析全局变量。这种方式更加安全,因为您无法更改您没有编写的代码的环境(除非使用调试库),同时更加灵活,因为您可以通过创建具有有限范围的local _ENV变量来更改代码块的环境。

然而,在任何情况下,你都需要一个默认环境,它会在程序员有机会设置自定义环境之前使用(或者如果程序员不想更改它)。Lua在启动时为你创建这个默认环境(也称为"全局环境"),并将其存储在注册表中。除非你向loadloadfile传递自定义环境,否则默认环境将用作所有块的_ENV upvalue。lua_pushglobaltable还直接从注册表检索此全局环境,因此所有C模块自动使用它来访问全局变量。

如果已经加载了标准的“基础”C模块,则默认的“全局环境”将有一个名为_G的表字段,该字段指向全局环境。
总之:
  • 全局变量_G实际上是_ENV._G
  • _ENV不是全局变量,而是一个upvalue或本地变量。
  • 默认的“全局环境”的_G字段指向全局环境本身。
  • 默认情况下,_G_ENV引用同一个表(即全局环境)。
  • C代码不使用任何一个,而是使用注册表中的一个字段(再次通过定义指向全局环境)。
  • 你可以替换全局环境中的_G而不会破坏C模块或Lua本身(但如果不小心可能会破坏第三方Lua模块)。
  • 你可以随时替换_ENV,因为它只影响你自己的代码(最多影响当前的块/文件)。
  • 如果你替换了_ENV,你可以自己决定在受影响的代码中是否可用_G_ENV._G),以及它指向哪里。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接