为什么这个委托不在循环中工作?

时间:2012-07-17 14:36:36

标签: c# generics delegates

  

可能重复:
  C#: using the iterator variable of foreach loop in a lambda expression - why fails?

我正在MSDN上阅读c#reference,我发现了这个..

http://msdn.microsoft.com/en-us/library/0yw3tz5k.aspx

在评论的最后,albionmike有一条评论 就像这样..

When you "catpure" a variable from an outer scope, some counter-intuitive things happen.
If you run this, you will get an IndexOutOfRange exception during the call f().
If you uncomment the two commented out lines of code, it will work as expected.
Hint: Captured Outer Variables have reference rather than value semantics

// Console Project
using System;
using System.Collections.Generic;
using System.Text;


namespace EvilDelegation
{
    delegate void PrintIt();

    class Program
    {

        static void Main(string[] args)
        {
            string[] strings = { "zero", "one", "two", "three", "four" };
            PrintIt f = null;
            for (int i = 0; i < strings.Length; ++i) {
                if (i == 2 || i == 3) {
                    // Can you see why this would not work?
                    f = delegate() { Console.WriteLine(strings[i]); };

                    // But this does...
                    //int k = i;
                    //f = delegate() { Console.WriteLine(strings[k]); };

                }
            }
            f();
        }
    }
}

我不明白,为什么第一个不起作用,第二个会不会?在第4行,他说:Captured Outer Variables have reference rather than value semantics 好的。但是在for循环中,我们将i定义为int当然是值类型,那么int类型如何保存引用?并且如果i不能保持引用,那意味着它存储了值,如果它存储了值,那么我不明白为什么第一个不能工作而第二个会不会? 我在这里错过了什么吗?

编辑:我认为原作者有一个错字,对f()的调用应该在if循环中。在回答时请考虑这一点。

编辑2:好的,如果有人可能会说,这不是一个错字,让我们考虑一下。我想知道在f()子句中调用if的情况。两者都会在那种情况下运行,还是仅仅是没有评论的那个?

3 个答案:

答案 0 :(得分:4)

这是因为闭包的语义。当闭包引用其外部作用域中的局部变量时,它会捕获对变量的引用,而不是变量中包含的值。

在这种情况下,匿名代理delegate() { Console.WriteLine(strings[i]); }正在捕获 {/ 1}}变量的引用;也就是说,变量在匿名函数和声明i的范围之间共享。当i在一个上下文中发生变化时,它也会在另一个上下文中发生变化。

例如(see it run):

i

这将输出:

using System;

class Foo {
    static void Main() {
        int i = 0;
        Action increment = delegate { ++i; };

        Console.WriteLine(i);

        ++i;
        Console.WriteLine(i);

        increment();
        Console.WriteLine(i);

        ++i;
        Console.WriteLine(i);

        increment();
        Console.WriteLine(i);
    }
}

在C#中,本地的生命周期被扩展为包括引用它们的任何闭包的生命周期。这使得一些非常有趣的技巧可能会冒犯C / C ++开发人员的敏感性:

0
1
2
3
4

答案 1 :(得分:3)

您正在访问修改后的闭包。您的委托仅在被调用时进行评估,并且已捕获循环变量i。在访问此时,退出循环后,其值将等于strings.Length。但是,通过在循环中引入局部变量k,您将捕获循环迭代的特定变量k并且结果是正确的。

如果在循环内进行调用,正如您在下面的评论中所建议的那样,那么i的值将在循环前进之前评估,并具有“正确”值。

答案 2 :(得分:0)

这只是(不幸)设计。当您在委托中使用循环变量i时,它会被捕获,i在您到达f()来电时会有最终价值。

根据Eric Lippert的this blog postforeach变量很快就会发生变化,但for变量不会变化(这是你的例子中的变量)。

<强>增加:

以下是使用foreach代替for的示例:

  string[] strings = { "zero", "one", "two", "three", "four" }; 

  var list = new List<PrintIt>();

  foreach (var str in strings)
  {
    PrintIt f = delegate { Console.WriteLine(str); };  // captures str
    list.Add(f);
  }
  var f0 = list[0];
  f0();

在我的.NET版本(.NET 4.0,C#4)中,最后一行打印“四”。在即将推出的.NET版本(.NET 4.5,C#5,Visual Studio 2012)中,根据我的理解,它将打印为“零”。 突破变化......

当然,在这种情况下,delegate { Console.WriteLine(str); }相当于delegate() { Console.WriteLine(str); },相当于() => { Console.WriteLine(str); }