C# Closures, why is the loopvariable captured by reference?

前端 未结 6 1508
南笙
南笙 2020-11-30 11:39

In this example, I\'m attempting to pass by value, but the reference is passed instead.

for (int i = 0; i < 10; i++)
{
    Thread t = new Thread(() =>         


        
相关标签:
6条回答
  • 2020-11-30 11:57

    When using an anonymous delegate or a lambda expression a closure is created so outside variables can be referenced. When the closure is created the stack (value) variables are promoted to the heap.

    One way to avoid this is to start the thread with a ParameterizedThreadStart delegate. E.G.:

            static void Main()
        {
    
            for (int i = 0; i < 10; i++)
            {
                bool flag = false;
    
                var parameterizedThread = new Thread(ParameterizedDisplayIt);
                parameterizedThread.Start(flag);
    
                flag = true;
            }
    
            Console.ReadKey();
        }
    
        private static void ParameterizedDisplayIt(object flag)
        {
            Console.WriteLine("Param:{0}", flag);
        }
    

    Coincidentally I ran into this concept just yesterday: Link

    0 讨论(0)
  • 2020-11-30 12:02

    Well, that's just how C# works. The lambda expression in your statement constructs a lexical closure, which stores a single reference to i that persists even after the loop has concluded.

    To remedy it, you can do just the thing that you did.

    Feel free to read more on this particular issue all around the Web; my choice would be Eric Lippert's discussion here.

    0 讨论(0)
  • 2020-11-30 12:13

    Short answer: closures. Long answer given here (among other places): Differing behavior when starting a thread: ParameterizedThreadStart vs. Anonymous Delegate. Why does it matter?

    0 讨论(0)
  • 2020-11-30 12:16

    It happens because of the way C# passes parameters to a lambda. It wraps the variable access in a class which is created during compilation, and exposes it as a field to the lambda body.

    0 讨论(0)
  • 2020-11-30 12:17

    This is easier to understand if you look at what happens, in terms of scope:

    for (int i = 0; i < 10; i++)
    {
        Thread t = new Thread(() => new PhoneJobTest(i);    
        t.Start();
    }
    

    Basically translates to something very close to this:

    int i = 0;
    while (i < 10)
    {
        Thread t = new Thread(() => new PhoneJobTest(i);    
        t.Start();
        i++;
    }
    

    When you use a lambda expression, and it uses a variable declared outside of the lambda (in your case, i), the compiler creates something called a closure - a temporary class that "wraps" the i variable up and provides it to the delegate generated by the lambda.

    The closure is constructed at the same level as the variable (i), so in your case:

    int i = 0;
    ClosureClass = new ClosureClass(ref i); // Defined here! (of course, not called this)
    while (i < 10)
    {
        Thread t = new Thread(() => new PhoneJobTest(i);    
        t.Start();
        i++;
    }
    

    Because of this, each Thread gets the same closure defined.

    When you rework your loop to use a temporary, the closure is generated at that level instead:

    for (int i = 0; i < 10; i++)
    {
        int jobNum = i;
        ClosureClass = new ClosureClass(ref jobNum); // Defined here!
        Thread t = new Thread(() => new PhoneJobTest(jobNum);    
        t.Start();
    }
    

    Now, each Thread gets its own instance, and everything works properly.

    0 讨论(0)
  • 2020-11-30 12:17

    You definitely want to read Eric Lippert's "Closing over the loop variable considered harmful":

    • Part 1
    • Part 2

    In Short: The behavior you see is exactly how C# works.

    0 讨论(0)
提交回复
热议问题