15

Suppose we have the following code:

void AFunction()
{

   foreach(AClass i in AClassCollection)
   {
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(i.name); }  );
   }
}

void Main()
{
    AFunction();
    foreach( var i in listOfLambdaFunctions)
       i();
}

One might think that the above code would out the same as the following:

void Main()
{

    foreach(AClass i in AClassCollection)
       PrintLine(i.name);
}

However, it doesn't. Instead, it prints the name of the last item in AClassCollection every time.

It appears as if the same item was being used in each lambda function. I suspect there might be some delay from when the lambda was created to when the lambda took a snapshot of the external variables used in it. Essentially, the lambda is holding a reference to the local variable i, instead of taking a "snapshot" of i's value when the lambda was created.

To test this theory, I tried this code:

string astr = "a string";
AFunc fnc = () => { System.Diagnostics.Debug.WriteLine(astr); };
astr = "changed";
fnc();

and, surprise, it outputs changed!

I am using XNA 3.1, and whichever version of C# that comes with it.


My questions are:

  1. What is going on?
  2. Does the lambda function somehow store a 'reference' to the variable or something?
  3. Is there any way around this problem?
yummypasta
  • 1,398
  • 2
  • 17
  • 36
matt
  • 4,042
  • 5
  • 32
  • 50
  • 1
    Related: http://stackoverflow.com/questions/271440/c-captured-variable-in-loop – Ani Nov 11 '10 at 14:59
  • 1
    How can it output "changed!" when you assigned "chagnged!"? :p – GôTô Oct 11 '16 at 11:48
  • "Closures close over variables, not over values" rule is applied here, so don't expect value of astr to be copied in that lambda function, you copied the variable so, in this case it's a reference to that variable, and you had already proved it by your own observations. – Onur Dec 01 '16 at 06:25

5 Answers5

20

This is a modified closure

See: similar questions like Access to Modified Closure

To work around the issue you have to store a copy of the variable inside the scope of the for loop:

   foreach(AClass i in AClassCollection) 
   { 
      AClass anotherI= i;
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(anotherI.name); }  ); 
   } 
Community
  • 1
  • 1
Chris Baxter
  • 16,083
  • 9
  • 51
  • 72
  • ahh yes! such an obvious solution now its pointed out to me :D. I always forget that technically a variable like that is created and cleared each loop (in c++) or simply a new 'variable' is created each loop. mainly because it makes the optimal code nazi inside me weep at the very thought of that actually happening :), certainly handy here though! – matt Nov 11 '10 at 15:09
  • 1
    @matt, the optimal code nazi should chill out a bit, after considering that just because such variables are created afresh each loop doesn't mean they have to be implemented as such in the compiled version if it isn't necessary. At the same time, it does allow the above to work. Best of both worlds. – Jon Hanna Nov 11 '10 at 17:06
16

does the lambda function somehow store a 'reference' to the variable or something?

Close. The lambda function captures the variable itself. There is no need to store a reference to a variable, and in fact, in .NET it is impossible to permanently store a reference to a variable. You just capture the entire variable. You never capture the value of the variable.

Remember, a variable is a storage location. The name "i" refers to a particular storage location, and in your case, it always refers to the same storage location.

Is there anyway around this problem?

Yes. Create a new variable every time through the loop. The closure then captures a different variable every time.

This is one of the most frequently reported problems with C#. We're considering changing the semantics of the loop variable declaration so that a new variable is created every time through the loop.

For more details on this issue see my articles on the subject:

http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • In terms of the cost/benifit calculations that you talk about at times Eric, how would a change to this be worth more then say, a bug fix or new feature? – asawyer Nov 12 '10 at 13:58
  • @asawyer: that is a good question. This is probably the number one "false" bug report that we get. It confuses a lot of people. Code that closes over a loop variable is brittle, and almost no one actually *wants* the specified behaviour. And we are going to be doing major surgery to the closure code anyway to make "await" work; we can fix this while we're at it. Those are all good reasons to take action. On the con side, it is a breaking change, and it is a distraction from more important work. The design team will at some point make a recommendation one way or the other I'm sure. – Eric Lippert Nov 12 '10 at 15:11
5

what is going on? does the lambda function somehow store a 'reference' to the variable or something?

Yes exactly that; c# captured variables are to the variable, not the value of the variable. You can usually get around this by introducing a temp variable and binding to that:

string astr = "a string";
var tmp = astr;
AFunc fnc = () => { System.Diagnostics.Debug.WriteLine(tmp); };

especially in foreach where this is notorious.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
2

Yes, the lambda stores a reference to the variable (conceptually speaking, anyway).

A very simple workaround is this:

 foreach(AClass i in AClassCollection)
   {
      AClass j = i;
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(j.name); }  );
   }

In every iteration of the foreach loop, a new j gets created, which the lambda captures. i on the other hand, is the same variable throughout, but gets updated with every iteration (so all the lambdas end up seeing the last value)

And I agree that this is a bit surprising. :)

jalf
  • 243,077
  • 51
  • 345
  • 550
1

I've been caught by this one as well, as said by Calgary Coder, it is a modified closure. I really had trouble spotting them until I got resharper. Since it is one of the warnings that resharper watches for, I am much better at identifying them as I code.

Matthew Vines
  • 27,253
  • 7
  • 76
  • 97