【问题标题】:Implementation of closures in Lua?在 Lua 中实现闭包?
【发布时间】:2011-12-08 13:08:31
【问题描述】:

我有一个关于如何实现闭包的问题。

假设这是在一个名为 test.lua 的文件中:

local a = 'asdf'

local function b()
    return a
end

a = 10

return b

另一个文件会这样做

a = require 'test'
a()

它会打印出来

10

如果a 是堆栈上指向'asdf' 的指针(我假设在堆上,但这没关系),并且创建闭包b,那么大概是a 中的地址保存给b 使用,a = 10 如何也改变闭包内的指针?

维基百科很好地说明了我的困惑:

如果其运行时内存模型在线性堆栈上分配所有局部变量1,则该语言实现无法轻松支持完全闭包。在此类语言中,函数的局部变量会在函数返回时被释放。

我在想也许b 确实没有保存指向'asdf' 的指针,而是将堆栈偏移量保存到a,这样您就可以更改a 并且堆栈偏移量会将您带到a它指向您设置a 的最后一件事,但是当a(指针)从堆栈中弹出并且堆栈偏移量变得无效时,它是如何工作的?

1 我知道 Lua 不会在堆栈上分配 ,但它会将堆栈上的本地指针分配给堆中的值,不是吗?

【问题讨论】:

  • 我更改了标题和标签以使这个问题成为 LUA 特定的。闭包的实现方式(以及是否支持闭包)是非常特定于语言的。
  • @pst lua 是用 C 实现的,所以我认为它是相关的。
  • 事情是,C [only] 编码人员不会知道或关心——“这是 LUA 的事情”;-)

标签: lua closures implementation


【解决方案1】:

我真的希望你能更合理地命名这些变量。所以我会:

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/endwhile/do/endrepeat/untildo/endfor/endfunction/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 并执行两次返回的函数,您将获得两个独立的函数,它们分别引用了两个独立 堆栈。两个函数都不会看到另一个函数的值。

请注意,这可能并不完全是它的实现方式,但这就是它背后的总体思路。

【讨论】:

  • 啊哈,我明白了。那么,如果每个函数都存储自己的范围,并且编译器将局部变量的名称转换为该范围内的偏移量,那么堆栈上永远不会有任何局部变量,因为它们根本不存储在那里,对吗? (不包括为函数调用等推送它们)
  • 而且,许多范围可以与一个功能相关联,对吗?它是自己的,以及它所引用的 upvalue 所属的范围。
  • @SethCarnegie:local 变量是特定范围内的变量。该范围堆栈;堆栈是一个对象。退出范围时,对该堆栈的引用将丢失。如果没有其他人拥有该堆栈的引用(如果没有仍然存在的函数引用该堆栈),则可以收集该堆栈。闭包存储堆栈的一部分。人们会期望一个智能编译器可以判断一个函数何时存储堆栈的一部分,何时不存储,因此它可以优化掉简单的情况。
  • @SethCarnegie:一个函数的内部堆栈(它从它获得自己的local 变量的那个)像正常工作一样工作。它在函数被调用时出现,在函数退出时消失。它引用的外部堆栈是函数对象本身的一部分,但函数的执行堆栈会在每次函数调用时重建。
  • 我的意思是全局堆栈。我不知道每个函数都有自己的堆栈。或者更确切地说,我不知道该范围被称为堆栈,因为它似乎是一个固定大小,因为您确切知道在“编译”时将拥有多少个局部变量。所以,整个程序都有一个全局堆栈,每个函数在被调用时都有一个迷你堆栈(范围事物),对吗?感谢您对我的包容。
猜你喜欢
  • 2010-11-27
  • 2017-12-30
  • 2012-06-14
  • 2013-06-23
  • 2020-10-11
  • 2016-04-04
  • 2013-06-27
  • 1970-01-01
  • 2021-10-15
相关资源
最近更新 更多