Lua 变量作用域讨论

lua-users home
wiki

此页面试图用常见的变量作用域术语来描述 Lua 的变量作用域,但最终引发了关于变量作用域术语的争论。有关用通俗易懂的语言描述 Lua 变量作用域,请参见 LuaScoping

VersionNotice: 此页面上的评论指的是一个相当旧版本的 Lua(2001 年左右),该版本具有更有限的词法作用域,并且可能没有完整的闭包支持。这些评论中的许多内容不适用于最近版本的 Lua。

Lua 是静态作用域的 [1]

有限的静态作用域

关于从嵌套函数访问变量存在一个问题。嵌套函数无法访问其周围作用域中的变量,除了全局变量。提供了一种特殊的“upvalue”语法,用于只读访问变量的副本(在匿名函数实例化时),但只能从定义嵌套函数的直接作用域访问。

关于这种情况是否应该被称为有限/错误的静态/词法作用域存在一些分歧。一些计算机语言教材将词法作用域和静态作用域定义为相同,这使问题变得更加复杂。当访问的变量位于全局作用域时,Lua 的行为与词法作用域一致。

do
    -- assume "a" is a global with value 5
    local foo = function() print(a) end
    a = 10
    foo() -- prints "10"
end

当使用 upvalue 访问下一个外层作用域时,Lua 的行为也与词法作用域一致,只要变量没有重新绑定(无论是从变量的作用域还是从嵌套函数)。

do
    local a = 5
    local foo = function() print(%a) end
    foo() -- prints "5"
end

无论如何,Lua 可能不像 Perl 这样的其他脚本语言那样糟糕,尤其是在它们早期的时候。Perl 最初使用动态作用域。它逐渐演变成通过 my 限定符支持词法作用域。词法作用域可能会成为 Perl 6 的默认设置。

Python 以前也有类似于 Lua 的有限静态作用域。词法作用域将成为 2.2 版本的默认设置,并且在 2.1 版本中已经是可选的 [2]。Python 中词法作用域的实现不允许从嵌套函数内部重新绑定变量,这与 Lua 的 upvalue 类似。

对于 Lua,也许只需要将它有限的子集扩展到真正的词法作用域。它可以继续使用 upvalue 概念,并且不允许像 Python 那样重新绑定。或者它可以允许重新绑定,并消除对 upvalue 的需求,因为 upvalue 往往是 Lua 程序员混淆的来源。在 Python 词法作用域的设计文档中,不允许多次绑定的主要原因是 Python 缺乏变量声明。由于 Lua 要求使用 local 来限定变量,因此这将不是问题。

备注

顺便说一下,Lua *始终* 使用词法作用域。没有例外。没有动态作用域规则。编译器决定使用哪个名称声明,如果它无法处理这种情况(外部函数中的局部变量),则会发出错误。因此,问题不在于 Lua 有时是否使用词法作用域,而是它始终使用词法作用域。Lua 只是对允许的内容有一些限制。(词法/动态作用域定义了*如何*进行名称解析,而不是允许什么。)

-- E. Toernig

你得跟 John Ramsdell 讨论那个论点,我就不参与了 :)。我并不否认 Lua 是静态作用域的。混淆是因为有些人将“静态作用域”和“词法作用域”区分开来。--JohnBelmonte

我有点困惑。我认为我理解“静态”和“动态”作用域之间的区别。人们将“静态”和“词法”区分开来的区别是什么?--JamesHearn

我认为混淆源于在通用用法中对词法作用域和静态作用域这两个术语存在两种不兼容的定义。函数式编程社区过去使用这些术语来表示包含嵌套作用域要求的内容,但是,龙书定义明确表明 Lua 同时具有词法作用域和静态作用域。-- John D. Ramsdell

摘自 Aho、Sethi 和 Ullman 于 1986 年出版的“编译器:原理、技术和工具”,第 411 页,Addison-Wesley

语言的作用域规则决定了对非局部名称引用的处理方式。一个常见的规则,称为*词法*或*静态作用域规则*,通过仅检查程序文本来确定适用于名称的声明。Pascal、C 和 Ada 是众多使用词法作用域的语言中的几种,它们还添加了“最紧密嵌套”的规定,将在下面讨论。另一种规则,称为*动态作用域规则*,通过考虑当前激活来确定运行时适用于名称的声明。Lisp、APL 和 Snobol 是使用动态作用域的语言中的几种。

将 FOLDOC 条目与龙书中的内容进行比较,龙书的定义似乎有点宽松,因为它没有将“最小块”规定作为必要条件,并且只说它是 C 等某些语言的属性。换句话说,只要给定的作用域规则不是动态的(即不依赖于当前激活),就可以被认为是静态的。

我对“有限静态作用域”和“真正词法作用域”等术语的含义感到困惑。语言要么是静态作用域的,要么不是静态作用域的,要么是词法作用域的,要么不是词法作用域的。在这些术语前面添加修饰词毫无意义。-- John D. Ramsdell

Lua 目前缺乏静态嵌套作用域

在 Lua 语言中,每个变量要么具有局部作用域,要么具有全局作用域。函数可以在函数内部定义,但是,嵌套函数无法访问其任何封闭函数中定义的变量。因此,Lua 变量缺乏静态嵌套作用域。因此,以下 Lua 代码是非法的

function addn(x)
  function sum(y)
    return x+y
  end
  return sum
end
print((addn(3))(1))

此示例是非法的,因为在 addn 中定义的变量 x 无法被嵌套函数 sum 访问。

嵌套函数可以访问其直接封闭函数中定义的变量的副本。对函数内变量的 upvalue 引用会在函数求值时提取此副本以生成闭包。以百分号开头的变量引用表示 upvalue 引用。在 sum 中的 x 引用前面加上百分号,使上面的示例成为合法的 Lua 代码。

这种对静态嵌套作用域的糟糕替代方案之所以被采用,是因为它使实现变得容易,在该实现中,所有非全局变量都分配在堆栈中,但是,不必放弃静态嵌套作用域来拥有分配在堆栈中的非全局变量。

2.2 版之前的 Python 语言具有类似于 Lua 当前规则的作用域规则——每个变量要么具有局部作用域,要么具有全局作用域。从 2.2 版开始,Python 具有静态嵌套作用域。在 Python 的实现中,从嵌套函数引用的非全局变量是不可变的。有了这个额外的假设,分配在堆栈中的非全局变量很容易实现。

Python 的实现证明了这种方法的有效性。由于使用了扁平闭包,它们具有快速实现,如 Luca Cardelli 在 1984 年发表的题为“编译函数式语言”的论文中所述[3]。相关部分是关于“获取变量”的第 4 部分。

对于我们这些希望未来版本的 Lua 具有具有静态嵌套作用域的变量的人来说,我建议我们可以通过寻找核心 Lua 实现者可以使用的实现技术来提供帮助。我认为我们应该仔细研究 Python 2.2 实现的相关部分以获取想法,并将其公开。

我不是 Python 程序员,但这些评论让我可以查看 Python 的作用域规则。如果我有什么错误,请纠正我。据我了解,Python 2.0 与 Lua 在嵌套函数方面存在完全相同的问题。无法访问外部局部变量,也无法使用局部递归函数。传递变量到局部函数的方法是通过以name=name形式命名的参数完成的。第一个name是函数的形式参数名,第二个name表示在函数声明时冻结的外部作用域的值。因此,这与 Lua 中的 upvalue 具有相同的行为。这只是语法不同:在 Python 中,您在参数列表中添加foo=foo来访问外部foo,在 Lua 中,您在函数中编写%foo来访问外部变量。语义相同。特别是,您无法更改外部变量的值,并且对外部局部的后续更改将不会对已声明的函数可见。Lua 有一个奇怪的限制(可以轻松地移除,并且不会造成向后兼容性问题),即您只能访问直接封闭的作用域。我想 Python 也存在同样的问题——您必须将值从一个函数传递到另一个函数。

在 Python 2.2 中,他们改变了作用域规则,以便编译器自动生成等效于 *name=name* 参数的内容。你仍然无法改变外部名称的值。但不知何故,他们现在可以递归地调用局部函数。(两个局部函数相互调用会怎样?这行得通吗?)也许这与局部函数之后声明的局部变量对函数可见的奇特现象有关。但这仍然不是像 Pascal 或 Scheme 这样的语言中发现的词法作用域。

我发布到 lua-l 的名为“可写上值”的补丁(针对 Lua 3.2)实现了 Python 2.2 的外部局部语义。唯一缺少的魔法是递归函数调用。但似乎这不是大多数人想要看到的(包括我自己 - 主要是因为递归函数调用问题)。

-- ET

我相信用良好的高级语言编写的程序应该很容易被只熟悉该语言的人阅读。除了上值之外,我认为 Lua 在这方面表现出色,因为它采用了类似 Pascal 的语法来表示控制结构,其含义正是人们所期望的。使用上值的程序不太可能被普通用户理解。需要手册才能理解它们何时获取其值。大多数人直观地理解静态嵌套作用域。当从嵌套函数中引用变量时,使变量不可变会给 Lua 程序的作者带来负担,但不会给程序的读者带来负担。我认为 Lua 应该设计成首先满足读者的需求。

以下是静态嵌套作用域的定义

在静态嵌套作用域语言中,标识符的作用域在编译时固定为包含标识符声明的最小块(begin/end 或 function/procedure 主体)。这意味着在某个块中声明的标识符只能在该块内以及从该块中声明的程序中访问。

-- John D. Ramsdell

参考资料

来自 *如何设计程序*(基于 Scheme 的教学文本)

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2008 年 3 月 29 日下午 11:49 GMT (差异)