Integer handled as reference type when passed into

2019-01-28 07:03发布

I was attending the TechDays 2013 in the Netherlands this week and I got an interesting quiz question presented. The question was: What is the output of the following program. Here is what the code looks like.

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

Obviously, the answer I gave was wrong. I argumentend, because int is a value type, the actual value that is passed into Console.WriteLine() gets copied, so the output would be 0...9. However i is handled as a reference type in this situation. The correct answer is that it will display ten times 10. Can anyone explain why and how?

标签: c# delegates
4条回答
萌系小妹纸
2楼-- · 2019-01-28 07:48

It is because it is a captured variable. Note that this used to also happen with foreach, but that changed in C# 5. But to re-write your code to what you actually have:

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

As you can see: there is only one ctx thus only one ctx.i, and it is 10 by the time you foreach over writers.

Btw, if you want to make the old code work:

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

Basically, the capture-context is scoped at the same level as the variable; here the variable is scoped inside the loop, so this generates:

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

Here each DoStuff is on a different capture-context instance, so has a different and separate i.

查看更多
smile是对你的礼貌
3楼-- · 2019-01-28 07:49

I argumentend, because int is a value type, the actual value that is passed into Console.WriteLine() gets copied

That is exactly correct. When you call WriteLine the value will be copied.

So, when are you calling WriteLine? It's not in the for loop. You're not writing anything at that point in time, you're just creating a delegate.

It's not until the foreach loop when you invoke the delegate, it's at that time that the value in the variable i is copied to the stack for the call to WriteLine.

So, what's the value of i during the foreach loop? It's 10, for each iteration of the foreach loop.

So now you're asking, "well how is i anything during the foreach loop, isn't it out of scope. Well, no, it's not. What this is demonstrating is a "closure". When an anonymous method reference a variable that variable's scope needs to last for as long as that anonymous method, which could be for any period of time. If nothing special is done at all reading the variable would be random garbage containing whatever happened to be stuck in that location in memory. C# actively makes sure that situation can't happen.

So what does it do? It creates a closure class; it's a class that will contain a number of fields representing everything that is closed over. In other words, the code will be refactored to look something like this:

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

Now we both have a name for our anonymous method (all anonymous methods are given a name by the compiler) and we can ensure that the variable will live for as long as the delegate that refers to the anonymous function lives.

Looking at this refactor, I hope it's clear why the result is that 10 is printed 10 times.

查看更多
混吃等死
4楼-- · 2019-01-28 07:54

In your case, the delegated methods are anonymous methods accessing a local variable (the for loop index i). That is, these are clousures.

Since the anonymous method is called ten times after the for loop, it gets the most recent value for i.

A simple sample of various clousures accessing the same reference

Here's a simplified version of clousure behavior:

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!

查看更多
劳资没心,怎么记你
5楼-- · 2019-01-28 07:55

For me, it's easier to understand by comparing the old behavior and the new behavior with the native Action class in place of a custom Writer.

Before C# 5 closure captured the same variable (not the value of the variable) in cases of for, foreach variables and local variable captures. So given the code:

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

We see just the last value that we set for the variable forLoopVariable. However, with C# 5, the foreach loop has been modified. Now we capture distinct variables.

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

So the output is more intuitive: 0,1,2...

Note that this is a breaking change (albeit its assumed to be a minor one). And that may be why the for loop behavior remains unchanged with C# 5.

查看更多
登录 后发表回答