5

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

C
C
C

但 LINQ 等价物产生的输出为

甲乙丙
_
_

我理解第一组输出 - 它在退出循环时采用最后一个值,但在我看来,传统循环和 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();
        }
    }
4

2 回答 2

7

这是因为您正在关闭一个循环变量。我无法比 Lippert 更好地解释它。(任何人都可以吗?)如果您仍然感到困惑,请花一些额外的时间来思考它并阅读他博客上的评论——它们应该很有启发性。

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

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

问题就会消失。

于 2013-06-01T02:30:31.163 回答
4

I wanted to add to Dave Markle's explanation. He's absolutely right when he says this is because of "closing over a loop variable". To understand why this happens you have to go back to how closures work with delegates. Take a look at the following simple case without loops:

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();
    }
}

What actually prints out here is "B" not "A". The reason is because the reference name points to has changed and when you called test().

When C# compiles your code it's magic sauce essentially changes your lambda expressions into delegates such as the one in the code above, and underneath the hood your name variable is just a reference, when name changes the call to test() will return a different result. As you've looped through, the last item in the list was what name was set to last and therefore when the action is finally invoked the name is only pointing to the last item in the list which is what gets printed. I hope my explanation isn't too verbose.

Just imagine that when if we changed everything into for loops this is what C# would see:

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();
    }
}
于 2013-06-01T02:48:38.587 回答