3

本周我参加了在荷兰举行的 TechDays 2013,我收到了一个有趣的测验问题。问题是:以下程序的输出是什么。这是代码的样子。

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        for (int i = 0; i < 10; i++)
        {
            writers.Add(delegate { Console.WriteLine(i); });
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

显然,我给出的答案是错误的。我论证结束,因为 int 是一种值类型,所以传入的实际值Console.WriteLine()被复制,所以输出将是 0...9。然而i,在这种情况下被作为引用类型处理。正确答案是它会显示 10 乘以 10。谁能解释为什么以及如何?

4

4 回答 4

6

我争论结束,因为 int 是一个值类型,传递给 Console.WriteLine() 的实际值被复制

这是完全正确的。 当您调用时WriteLine,该值将被复制

那么,你什么时候打电话WriteLine?它不在for循环中。在那个时间点你没有写任何东西,你只是在创建一个委托。

直到foreach您调用委托时的循环,才将变量中的值i复制到堆栈中以调用WriteLine.

那么,在循环i期间的价值是什么?foreach对于循环的每次迭代,它是 10 foreach

所以现在你问,“好吧,i在.foreach loop, isn't it out of scope匿名方法,可以是任何时间段。如果根本没有做任何特别的事情,读取变量将是随机垃圾,其中包含发生在内存中该位置的任何内容。C# 积极确保这种情况不会发生。

那么它有什么作用呢?它创建了一个闭包类;它是一个包含许多字段的类,这些字段代表所有已关闭的内容。换句话说,代码将被重构为如下所示:

public class ClosureClass
{
    public int i;

    public void DoStuff()
    {
        Console.WriteLine(i);
    }
}

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        ClosureClass closure = new ClosureClass();
        for (closure.i = 0; closure.i < 10; closure.i++)
        {
            writers.Add(closure.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

现在我们都有一个匿名方法的名称(所有匿名方法都由编译器命名),并且我们可以确保该变量将在引用匿名函数的委托存在时持续存在。

看看这个重构,我希望很清楚为什么结果是10打印了 10 次。

于 2013-03-08T20:36:29.810 回答
4

这是因为它是一个捕获的变量。请注意,这曾经也发生过foreach,但在 C# 5 中发生了变化。但是要将代码重新编写为您实际拥有的代码:

class Program
{
    delegate void Writer();

    class CaptureContext { // generated by the compiler and named something
        public int i;      // truly horrible that is illegal in C#
        public void DoStuff() {
            Console.WriteLine(i);
        }
    }
    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        var ctx = new CaptureContext();
        for (ctx.i = 0; ctx.i < 10; ctx.i++)
        {
            writers.Add(ctx.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

如您所见:只有一个,ctx因此只有一个,当您结束ctx.i时,它是 10 。foreachwriters

顺便说一句,如果你想让旧代码工作:

for (int tmp = 0; tmp < 10; tmp++)
{
    int i = tmp;
    writers.Add(delegate { Console.WriteLine(i); });
}

基本上,捕获上下文与变量处于同一级别。这里变量在循环内的范围,所以这会生成:

for (int tmp = 0; tmp < 10; tmp++)
{
    var ctx = new CaptureContext();
    ctx.i = tmp;
    writers.Add(ctx.DoStuff);
}

这里每个DoStuff都在不同的捕获上下文实例上,因此有一个不同且独立的i.

于 2013-03-08T20:37:37.970 回答
1

在您的情况下,委托方法是访问局部变量(for循环索引)的匿名方法。也就是说,这些是clousuresi

由于匿名方法在for循环之后被调用了十次,因此它获得了i的最新值。

访问相同参考的各种 clousure 的简单示例

这是 clousure 行为的简化版本:

int a = 1;

Action a1 = () => Console.WriteLine(a);
Action a2 = () => Console.WriteLine(a);
Action a3 = () => Console.WriteLine(a);

a = 2;

// This will print 3 times the latest assigned value of `a` (2) variable instead
// of just 1. 
a1();
a2();
a3();

查看 StackOverflow 上的其他问答(What are clousures in .NET?)以获取有关什么是 C#/.NET clousures 的更多信息!

于 2013-03-08T20:37:12.077 回答
0

Action对我来说,通过将旧行为和新行为与本地类而不是 custom进行比较更容易理解Writer

在 C# 5 闭包之前,在 for、foreach 变量和局部变量捕获的情况下捕获了相同的变量(不是变量的值)。所以给定代码:

    var anonymousFunctions = new List<Action>();
    var listOfNumbers = Enumerable.Range(0, 10);

    for (int forLoopVariable = 0; forLoopVariable < 10; forLoopVariable++)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(forLoopVariable); });//outputs 10 every time.
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

我们只看到我们为变量设置的最后一个。但是,在 C# 5 中,foreach 循环已被修改。现在我们捕获不同的变量。 forLoopVariable

例如

    anonymousFunctions.Clear();//C# 5 foreach loop captures

    foreach (var i in listOfNumbers)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(i); });//outputs entire range of numbers
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

所以输出更直观:0,1,2...

请注意,这是一个重大更改(尽管它被假定为次要更改)。这可能就是为什么 for 循环行为在 C# 5 中保持不变的原因。

于 2013-03-08T20:57:38.943 回答