传统循环与LINQ - 不同的输出

时间:2013-06-01 02:22:29

标签: c# .net linq

在下面的C#代码中,为什么第一组打印产生

的输出
  

Ç
  ç
  C

但LINQ等效的产生

的输出
  

一个
  乙
  C

我理解第一组输出 - 当它退出循环时它需要最后一个值,但在我看来传统循环和LINQ等价物之间应该保持一致吗? - 在这两种情况下都应该打印CCC或ABC吗?

public static void Main(string[] str)
    {
        List<string> names = new List<string>() {"A", "B", "C"};

        List<Action> actions = new List<Action>();
        foreach(var name in names)
        {
            actions.Add(() => Console.WriteLine(name));
        }

        foreach(var action in actions)
        {
            action.Invoke();
        }

        List<Action> actionList = names.Select<string, Action>(s => () => Console.WriteLine(s)).ToList();

        foreach(var action in actionList)
        {
            action.Invoke();
        }
    }

2 个答案:

答案 0 :(得分:7)

这是因为你是closing over a loop variable。我根本无法解释它比Lippert更好。 (任何人都可以吗?)如果你仍然感到困惑,请花点时间思考一下并阅读他博客上的评论 - 他们应该具有启发性。

使用Linq时这是一个非常常见的错误。几乎每个人都做到了。在C#5.0(在Visual Studio 2012中使用)编译器中,此行为已更改,但如果您可以帮助它,仍应避免执行此操作。您可以将第一个循环重写为:

foreach(var name in names)
    {
        var currentName = name;
        actions.Add(() => Console.WriteLine(currentName));
    }

问题就会消失。

答案 1 :(得分:4)

我想补充Dave Markle的解释。当他说这是因为“关闭一个循环变量”时,他是绝对正确的。要理解为什么会发生这种情况,您必须回到关闭如何与代理一起工作。看一下没有循环的以下简单案例:

class Program
{

    delegate void TestDelegate();

    static void Main(string[] args)
    {
        List<string> names = new List<string>() { "A", "B", "C" };
        var name = names[0];

        TestDelegate test = () => { Console.WriteLine(name); };
        name = names[1];

        test();

        Console.ReadLine();
    }
}

这里打印的是“B”而不是“A”。原因是因为参考名称指向已更改,并且您调用了test()。

当C#编译你的代码时,它的神奇之处实际上是将你的lambda表达式更改为代理,例如上面代码中的代理,并且在引擎盖下你的name变量只是一个引用,当name更改时,对test()的调用将返回不同的结果。当您循环播放时,列表中的最后一项是设置为最后的名称,因此当最终调用该操作时,名称仅指向列表中的最后一项,即打印的内容。我希望我的解释不是太冗长。

想象一下,当我们将所有内容都更改为for循环时,这就是C#所看到的:

class Program
{

    static void Main(string[] args)
    {
        List<string> names = new List<string>() { "A", "B", "C" };

        List<Action> actions = new List<Action>();

        string name = names[0];

        Action test = () => Console.WriteLine(name);

        for (int i = 0; i < names.Count; i++)
        {
            actions.Add(test);
        }

        name = names[1];
        foreach (var action in actions)
        {
            action.Invoke(); // Prints "B" every time because name = names[1]
        }

        Console.ReadLine();
    }
}