为什么C#不保留匿名委托调用的上下文?

5
我有以下方法:

我有以下方法:

static Random rr = new Random();
static void DoAction(Action a)
{
    ThreadPool.QueueUserWorkItem(par =>
    {
        Thread.Sleep(rr.Next(200));
        a.Invoke();
    });
}

现在我像这样在for循环中调用它:
for (int i = 0; i < 10; i++)
{
    var x = i;

    DoAction(() =>
    {
        Console.WriteLine(i); // scenario 1
        //Console.WriteLine(x); // scenario 2
    });
}

在场景1中,输出结果为:10 10 10 10 ... 10
在场景2中,输出结果为:2 6 5 8 4 ... 0(0到9的随机排列)

你如何解释这个问题?C#不应该为匿名委托调用保留变量(这里是i)吗?

2
但是保留(“捕获”)变量i恰好就是正在发生的事情!然而,虽然您希望发生的是保留设置委托时i的值,但实际上并没有发生。 - AakashM
2
顺便提一下,当您使用此代码时,ReSharper会警告您有一个“访问修改的闭包”(Access to modified closure)问题;您可能会发现那里的解释很有帮助。 - AakashM
http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx - Austin Salonen
3个回答

10
问题在于有一个变量 i,以及十个实例/拷贝的x。每个 lambda 表达式都会引用唯一的变量 i 和其中一个实例的 x。由于每个 x 只被写入一次,因此每个 lambda 表达式只能看到其所引用的那个值。
变量 i 会一直递增直到 10。所有 lambda 表达式在循环完成之前都不会运行,因此它们都会看到 i 的最终值为 10。
如果你这样重写它,我认为这个例子会更清晰易懂。
int i = 0;  // Single i for every iteration of the loop
while (i < 10) { 
  int x = i;  // New x for every iteration of the loop 
  DoAction(() => {
    Console.WriteLine(i);
    Console.WriteLine(x);
  });
  i++;
};

1
更加明确地说,每个 x 不仅仅只被写入一次,而且它们是不同的 x,因为每次都会重新初始化。在循环的生命周期中有 10 个 x,但只有一个 i - Jon Hanna

1

我认为你可以用Java或任何面向对象的语言得到相同的结果(不确定,但在这里似乎很合理)。

i的作用域是整个循环,而x的作用域是每个出现的情况。

Resharper可以帮助您发现这种问题。


1

DoAction 会生成一个线程并立即返回。当线程从其随机休眠中醒来时,循环将已经完成,并且 i 的值将一直增加到 10。另一方面,x 的值在调用之前被捕获和冻结,因此您将以随机顺序获取从 09 的所有值,这取决于每个线程根据您的随机数生成器睡眠多长时间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接