在 lambda 表达式中使用 foreach 循环的迭代变量,为什么会失败?

23

考虑以下代码:

public class MyClass
{
   public delegate string PrintHelloType(string greeting);


    public void Execute()
    {

        Type[] types = new Type[] { typeof(string), typeof(float), typeof(int)};
        List<PrintHelloType> helloMethods = new List<PrintHelloType>();

        foreach (var type in types)
        {
            var sayHello = 
                new PrintHelloType(greeting => SayGreetingToType(type, greeting));
            helloMethods.Add(sayHello);
        }

        foreach (var helloMethod in helloMethods)
        {
            Console.WriteLine(helloMethod("Hi"));
        }

    }

    public string SayGreetingToType(Type type, string greetingText)
    {
        return greetingText + " " + type.Name;
    }

...

}
在调用`myClass.Execute()`后,代码打印出以下意外的响应:
Hi Int32
Hi Int32
Hi Int32  
很明显,我期望看到的是"Hi String""Hi Single""Hi Int32",但实际上不是这样的。为什么在3个方法中都使用了迭代数组的最后一个元素,而非对应的元素呢?
你会如何重写代码以达成预期目标?

我甚至没有读题,但从标题上来看,我知道答案是:http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!689.entry - Brian
每天捕获的变量问题浮出水面。 - Marc
3个回答

31

欢迎来到闭包和捕获变量的世界 :)

Eric Lippert对这种行为进行了深入的解释:

基本上,被捕获的是循环变量本身,而不是它的值。 要获得您认为应该获得的结果,请执行以下操作:

foreach (var type in types)
{
   var newType = type;
   var sayHello = 
            new PrintHelloType(greeting => SayGreetingToType(newType, greeting));
   helloMethods.Add(sayHello);
}

5
@Eric Lippert的信号已被点亮。 - Anthony Pegram
3
只有Anders是神,Eric是他的先知 :) - SWeko
我想补充一下,即使是那些精通闭包的人也可能会被这个问题所困扰 - Lua 和其他语言在循环括号内部有 type。因此,在 Lua 中,您仍然捕获变量,但每次迭代都是一个新变量。这是在 Lua 编程中经常使用的东西 - 但是在我多年的 C# 编程中,我还没有编写过受益于其 type-is-outside-of-the-brackets 作用域的方法。有人吗? - Mania

8
作为对SWeko所提到的博客文章的简要说明,lambda表达式捕获的是变量而不是值。在foreach循环中,变量在每次迭代中都不是唯一的,相同的变量在整个循环期间都被使用(当您在编译时看到foreach扩展时,这更加明显)。因此,在每次迭代中都捕获了同一个变量,并且该变量(截至最后一次迭代)引用您集合的最后一个元素。
更新:在语言的新版本(从C# 5开始),循环变量在每次迭代中被视为新的,因此闭包不会产生与早期版本(C# 4及之前)相同的问题。

4
您可以通过引入额外的变量来解决这个问题:
...
foreach (var type in types)
        {
            var t = type;
            var sayHello = new PrintHelloType(greeting => SayGreetingToType(t, greeting));
            helloMethods.Add(sayHello);
        }
....

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