C#闭包未按预期工作

3

我不能够很清晰地理解两个代码块之间的区别。考虑下面这个程序:

    class Program
{
    static void Main(string[] args)
    {

        List<Number> numbers = new List<Number>
                                   {
                                       new Number(1),
                                       new Number(2),
                                       new Number(3)
                                   };


        List<Action> actions = new List<Action>();
        foreach (Number numb in numbers)
        {
            actions.Add(() => WriteNumber(numb));
        }

        Number number = null;
        IEnumerator<Number> enumerator = numbers.GetEnumerator();
        while (enumerator.MoveNext())
        {
            number = enumerator.Current;
            actions.Add(() => WriteNumber(number));
        }

        foreach (Action action in actions)
        {
            action();
        }

        Console.ReadKey();


    }

    public static void WriteNumber(Number num)
    {
        Console.WriteLine(num.Value);
    }

    public class Number
    {
        public int Value;

        public Number(int i)
        {
            this.Value = i;
        }

    }

}

输出结果为:
1
2
3
3
3
3    

这两个代码块应该是一样的。但你可以看到闭包在第一个循环中不起作用。我缺少什么?提前致谢。

这是foreach闭包问题吗?(https://dev59.com/v3RB5IYBdhLWcg3wyqEd) - sircodesalot
1
@sircodesalot:有点相反 - 这是由于C# 5修复了问题,所以foreach表现得很明智。 - Jon Skeet
哦,他们修复了。好的,知道了,谢谢! - sircodesalot
他们在C# 5.0中修复了它,尽管我使用的是3.5版本,但令人惊讶的是它能正常工作。 - user2843021
4个回答

3

你需要将number变量的声明放在while循环内部。每个数字的引用都被存储在number变量中,每次都会覆盖上一个值。

你应该将声明移动到while循环内部,这样你就有了每个数字的新变量。

    IEnumerator<Number> enumerator = numbers.GetEnumerator();
    while (enumerator.MoveNext())
    {
        Number number = enumerator.Current;
        actions.Add(() => WriteNumber(number));
    }

3
这两个代码块应该是完全相同的。
不,它们在C# 5中不同。实际上,在C# 3和4中它们是相同的。
但是在foreach循环中,在C# 5中,每次迭代都有一个变量。你的lambda表达式捕获了这个变量。后续的迭代会创建不同的变量,不会影响之前捕获的变量。
在while循环中,你只有一个变量,所有的迭代都会捕获它。对该变量的更改将在所有捕获它的委托中可见。可以通过在while循环后添加此行来查看:
number = new Number(999);

那么你的输出将是什么?
1
2 
3
999
999
999

现在在C# 3和4中,foreach规范基本上是设计上的缺陷 - 它会跨越所有迭代捕获单个变量。然后在C# 5中修复了这个问题,使用每个迭代的单独变量,这基本上是您在这种代码中始终想要的。


1
在你的循环中:
    Number number = null;
    IEnumerator<Number> enumerator = numbers.GetEnumerator();
    while (enumerator.MoveNext())
    {
        number = enumerator.Current;
        actions.Add(() => WriteNumber(number));
    }

number在循环范围之外声明。因此,当它被设置为下一个当前迭代器时,所有对number的操作引用也会更新到最新值。因此,当您运行每个操作时,它们都将使用最后一个数字。


0

感谢您的所有答案。但我认为被误解了。我想要闭包能够正常工作。这就是为什么我将循环变量设为超出范围所在的原因。问题是:为什么第一种情况下它不起作用?我忘了提到我使用的是 C#3.5(而不是 C#5.0)。所以soop变量应该定义在超出范围之外,两个代码块应该完全相同。


你真的应该把这个添加到你的问题中。这并不是一个真正的答案。 - juharr

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