我真的希望你能更合理地命名这些变量。所以我会:
local inner = 'asdf'
local function b()
return inner
end
inner = 10
return b
和
func = require 'test'
func()
好的,现在我们知道我们在说什么,我可以继续了。
Lua 块test 有一个名为inner 的局部变量。在该块中,您创建一个新函数b。由于这是一个新函数,它的作用域在块test 的范围内。
由于它在函数内,它有权访问在该函数之外声明的局部变量。但是因为它在函数内部,所以它不会像访问它自己的本地变量那样访问这些变量。编译器检测到inner 是一个声明在函数作用域之外的局部变量,因此它将其转换为 Lua 所谓的“上值”。
Lua 中的函数可以有任意数量的值(最多 255 个)与之关联,称为“upvalues”。在 C/C++ 中创建的函数可以使用 lua_pushcclosure 存储一些上值。 Lua 编译器创建的函数使用上值来提供词法作用域。
作用域是发生在固定 Lua 代码块中的所有内容。所以:
if(...) then
--yes
else
--no
end
yes 块有一个范围,no 块有一个不同的范围。在yes 块中声明的任何local 变量都不能从no 块中访问,因为它们超出了no 块的范围。
定义范围的 Lua 构造是 if/then/else/end、while/do/end、repeat/until、do/end、for/end 和 function/end。此外,每个称为 Lua“块”的脚本都有一个作用域。
范围是嵌套的。在一个范围内,您可以访问在更高范围内声明的局部变量。
“堆栈”表示在特定范围内声明为local 的所有变量。因此,如果您在某个范围内没有局部变量,则该范围的堆栈为空。
在 C 和 C++ 中,您熟悉的“堆栈”只是一个指针。当你调用一个函数时,编译器已经预先确定了函数的堆栈需要多少字节的空间。它使指针前进该量。函数中使用的所有堆栈变量都只是堆栈指针的字节偏移量。当函数退出时,堆栈指针会减少堆栈数量。
在 Lua 中,情况有所不同。特定范围的堆栈是一个对象,而不仅仅是一个指针。对于任何特定的范围,都为其定义了一些local 变量。当 Lua 解释器进入一个作用域时,它会“分配”一个堆栈,其大小是访问这些局部变量所必需的。所有对局部变量的引用都只是该堆栈的偏移量。从更高范围(先前定义)访问局部变量只需访问不同的堆栈对象。
因此,在 Lua 中,您在概念上拥有一堆堆栈(为清楚起见,我将其称为“s-stack”)。每个作用域都会创建一个新堆栈并将其推送,当您离开一个作用域时,它会将堆栈从 s-stack 中弹出。
当 Lua 编译器遇到对 local 变量的引用时,它会将该引用转换为 s 堆栈的索引,以及该特定堆栈的偏移量。所以如果它访问当前本地栈中的一个变量,s栈的索引是指s栈的顶部,偏移量是变量所在栈的偏移量。
这对于大多数访问范围的 Lua 构造来说都很好。但是function/end 不只是创建一个新范围;他们创建了一个新功能。并且这个函数被允许访问不只是该函数的本地堆栈的堆栈。
堆栈是对象。在 Lua 中,对象会被垃圾回收。当解释器进入一个作用域时,它会分配一个堆栈对象并推送它。只要堆栈对象被压入 s-stack,它就不能被销毁。堆栈的堆栈是指对象。但是,一旦解释器退出作用域,它就会将堆栈从 s 堆栈中弹出。因此,由于不再引用,因此有待收集。
但是,访问其本地范围之外的变量的函数仍然可以引用该堆栈。当 Lua 编译器看到对不在函数本地范围内的 local 变量的引用时,它会更改函数。它找出它所引用的本地堆栈属于哪个堆栈,然后将该堆栈作为上值存储在函数中。它将对该变量的引用转换为该特定上值的偏移量,而不是当前位于 s 堆栈上的堆栈的偏移量。
只要函数对象继续存在,它所引用的堆栈也将继续存在。
请记住,堆栈是在 Lua 解释器进入和退出函数范围时动态创建和销毁的。因此,如果您要运行test 两次,通过调用loadfile 并执行两次返回的函数,您将获得两个独立的函数,它们分别引用了两个独立 堆栈。两个函数都不会看到另一个函数的值。
请注意,这可能并不完全是它的实现方式,但这就是它背后的总体思路。