闭包对循环变量的语义是什么?

考虑以下 Lua 代码:

f = {}

for i = 1, 10 do
    f[i] = function()
        print(i .. " ")
    end
end

for k = 1, 10 do
    f[k]()
end

这将打印出数字 1 到 10。在这种情况下,i 的闭包是针对外部循环的每次迭代的值。这就是我一直理解闭包的方式,我很高兴...

...直到我将一些 Lua 代码移植到 C# 中,我尝试做同样的事情:

var f = new Action[10];

for (int i = 0; i < 10; i++)
{
    f[i] = (new Action(delegate()
    {
        Console.Write(i + " ");
    }));
}
for (int k = 0; k < 10; k++)
{
    f[k]();
}

现在我得到了数字 10 打印了 10 次(让我们忘记 Lua 数组是从 1 开始的)。实际上,在这种情况下,闭包是针对变量本身而不是其值的,这非常有意义,因为我只在第一个循环结束后调用函数。

JavaScript 似乎具有相同的语义(在变量上封闭):

var f = []

for (var i = 0; i < 10; i++)
{
    f[i] = function()
    {
        document.write(i + ' ');
    };
}

for (var k = 0; k < 10; k++)
{
    f[k]();
}

实际上,这两种行为都是有意义的,但当然是不兼容的。

如果有一种“正确”的方法来完成这个操作,那么 Lua、C#和 JavaScript 中的一种就是错误的(我还没有尝试过其他语言)。所以我的问题是:“在循环内捕获变量的“正确”语义是什么?”

编辑:我不是在问如何“修复”它。我知道我可以在循环内添加一个局部变量并封闭它,以在 C#/JavaScript 中获得 Lua 行为。我想知道封闭循环变量的理论正确含义是什么,以及哪些语言在每种方式中实现了闭包的简短列表。

编辑:重新表达我的问题:“在 lambda 演算中封闭循环变量的行为是什么?”

点赞
用户707111
用户707111

在编程中没有“正确”的方法,只有不同的方法。在C#中,你可以通过将变量设定在循环的作用域来修复它:

for (int i = 0; i < 10; i++)
{
    int j = i;

    f[i] = (new Action(delegate()
    {
        Console.Write(j + " ");
    }));
}

在JavaScript中,你可以通过创建并调用匿名函数来添加作用域:

for (var i = 0; i < 10; i++) {
    (function(i) {
        f[i] = function() {
            document.write(i + ' ');
        };
    })(i);
}

在C#中,迭代变量没有循环作用域。JavaScript没有块级作用域,只有函数作用域。它们只是不同的语言,它们的做法也不同。

2013-01-15 04:09:53
用户734069
用户734069

Lua 手册对此进行了详细解释。它将索引 for 循环描述为 while 循环,如下所示:

 for v = e1, e2, e3 do block end

-- 等同于:

 do
   local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
   if not (var and limit and step) then error() end
   while (step > 0 and var <= limit) or (step <= 0 and var >= limit) do
     local v = var
     block
     var = var + step
   end
 end

请注意,循环变量 v 是在 while 循环的作用域内声明的。这是特别为了允许你正在做的事情。

2013-01-15 04:14:36
用户83805
用户83805

在 lambda 演算中没有循环变量。

2013-01-15 16:57:09
用户501459
用户501459

闭包循环变量的处理方式与其他变量的处理方式类似。问题在于语言特定的循环结构以及它们是否翻译成把循环变量放在循环内部或外部的代码。例如,在 C#、Lua 或 JavaScript 中使用 while 循环时,这三种语言的结果都是相同的(10)。同样,在 JavaScript 或 C#中使用 for(;;) 循环时也是如此(不适用于 Lua)。

然而,如果使用 JavaScript 中的 for (i in x) 循环,则会发现每个闭包都会得到 i 的新副本(输出:“0 1 2 3 ...”)。在 Lua 中使用 for i=x,y 和 C# 中的 foreach 也是如此。这再次涉及到这些语言如何构造这些循环以及它们如何将循环变量的值暴露给循环体,而不是闭包语义的差异。

事实上,在 C# 的 foreach 中,这种行为 从4.5更改为5。这个结构:

 foreach (var x in l) { <loop body> }

以前翻译成(伪代码):

 E e = l.GetEnumerator()
 V v
 while (e.MoveNext()) {
      v = e.Current
      <loop body>
 }

在 C# 5 中,这被更改为:

 E e = l.GetEnumerator()
 while (e.MoveNext()) {
      V v = e.Current
      <loop body>
 }

这是一个破坏性的变化,为了更好地满足程序员对闭包循环变量的期望而进行的。闭包语义没有改变;循环变量的位置改变了。

2013-01-15 18:08:06