传递给委托时,整数作为引用类型处理

时间:2013-03-08 20:33:35

标签: c# delegates

本周我参加了荷兰的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.有人可以解释原因和方法吗?

4 个答案:

答案 0 :(得分:6)

  

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

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

那么,你什么时候打电话给WriteLine?它不在for循环中。你不是在那个时候写任何东西,你只是在创建一个代表。

当你调用委托时,直到foreach循环,那时变量i中的值被复制到堆栈以调用WriteLine

那么,i循环期间foreach的价值是多少?对于foreach循环的每次迭代,它都是10。

所以现在你问,“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次。

答案 1 :(得分: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,当您foreach超过writers时,它就是10。

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

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

答案 2 :(得分:1)

在您的情况下,委派的方法是匿名方法访问本地变量( for 循环索引i)。也就是说,这些是 clousures

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

访问相同参考的各种clousures的简单样本

以下是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();

Check this other Q&A (What are clousures in .NET?) on StackOverflow for more info about what are C#/.NET clousures!

答案 3 :(得分:0)

对我来说,通过将旧行为和新行为与原生Action类进行比较来代替自定义Writer,可以更容易理解。

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

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

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

E.G。

    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中保持不变。