循环变量上的闭包的正确语义是什么?

本文关键字:语义 是什么 闭包 变量 循环 | 更新日期: 2023-09-27 17:53:22

考虑以下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演算中关闭一个循环变量的行为是什么?"

循环变量上的闭包的正确语义是什么?

Lua手册解释了为什么这样做。它将索引for循环描述为while循环,如下所示:

 for v = e1, e2, e3 do block end
--Is equivalent to:
 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循环的范围内声明的。

没有"正确"的方法。有不同的方法。在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没有块作用域,只有函数作用域。它们只是不同的语言,它们做事情的方式不同。

" lambda演算中关闭一个循环变量的行为是什么?"

lambda演算中没有循环变量

关闭循环变量就像关闭任何其他变量一样。问题在于特定于语言的循环结构,以及它们是否被翻译成将循环变量置于循环内部或外部的代码。

例如,如果您在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>
 }

这是一个突破性的变化,这样做是为了更好地满足程序员在关闭循环变量时的期望。闭包语义没有改变;