在 Lua
语言中,全局变量并不一定非得是真正全局的。 Lua
语言甚至根本没有全局变量。由于我们在平时不断地使用全局变量,所以一开始听上去这可能很诡异。 Lua
语言竭尽全力地让程序员具有全局变量存在的幻觉。现在,让我们看看 Lua
语言是如何构建这种幻觉的。
提示
这种机制是 Lua5.1
和 Lua5.2
之间改变最大的部分,接下来的讨论基本都不适用与 Lua5.1
。
首先,让我们忘掉全局变量而从自由名称的概念开始讨论。一个自有名称是指没有关联到显式声明上的名称,即它不出现在对应局部变量的范围内。例如,在下面的代码中, x
和 y
是自由名称,而 z
则不是:
local z = 10 x = y + z
接下来就到了关键的部分: Lua
语言编译器将代码段中的所有自由名称 x
转换为 _ENV.x
。因此,此前的代码段完全等价于:
local z = 10 _ENV.x = _ENV.y + z
但是这里新出现的 _ENV
变量由究竟是什么呢?
我们刚刚才说过, Lua
语言中没有全局变量。因此 _ENV
不可能是全局变量。在这里,编译器实际上又进行了一次巧妙的工作。 Lua
语言把所有的代码段都当做匿名函数。所以, Lua
语言编译器实际上将原来的代码段编译为如下形式:
local _ENV = some value (某些值) return function (...) local z = 10 _ENV.x = _ENV.y + z end
也就是说, Lua
语言是在一个名为 _ENV
的预定义上值(一个外部的局部变量, upvalue
)存在的情况下编译所有的代码段的。因此,所有的变量要么是绑定到了一个名称的局部变量,要么是 _ENV
中的一个字段,而 _ENV
本身是一个局部变量(一个上值)。
_ENV
的初始值可以是任意的表(实际上也不用一定是表)。任何一个这样的表都被称为一个环境。为了维护全局变量存在的幻觉, Lua
语言在内部维护了一个表用来做全局环境。通常,当加载一段代码时,函数 load
会使用预定义的上值来初始化全局环境。因此,原始的代码等价于:
local _ENV = the global environment (全局环境) return function (...) local z = 10 _ENV.x = _ENV.y + z end
上述赋值的结果是,全局环境中的字段 x
得到全局环境中字段 y
加 10
的结果。
咋一看,这可能像是操作全局变量的一种相当拐弯抹角的方式。但是,这种方式比那些更简单的实现方法具有更多的灵活性。
我们先总结一下 Lua
语言中处理全局变量的方式:
- 编译器在编译所有代码段前,在外层创建局部变量
_ENV
- 编译器将所有自由名称
var
变换为_ENV.var
- 函数
load
或函数loadfile
使用全局环境初始化代码段的第一个上值,即Lua
语言内部维护的一个普通表。
实际上,这也不是太复杂。
有些人由于试图从这些规则中引申出额外的“魔法”而感到困惑,其实,这些规则并没有额外的定义。尤其是,前两条规则完全是由编译器进行的。除了是编译器预先定义的, _ENV
只是一个单纯的普通变量。抛开编译器,名称 _ENV
对于 Lua
语言来说根本没有特殊含义。类似的,从 x
到 _ENV.x
的转换是纯粹的语法转换,没有隐藏的含义。尤其是,在转换后,按照标准的可见性规则, _ENV
引用的是其所在位置所有可见的 _ENV
变量。
提示
Lua
语言将 _ENV
用于错误信息,以便与能够像报告涉及 global x
的一个错误一样报告涉及变量 _ENV.x
的错误。