question

DemitriusWheelwright-4098 avatar image
0 Votes"
DemitriusWheelwright-4098 asked DemitriusWheelwright-4098 commented

For Loop Variable Within IEnumerable Changes Values Outside of Scope

I am experiencing some odd behavior that I am not understanding.

The code sample below illustrates the problem.

When using a for loop and using the variable to set a property on an object - I store that object into the list.

When the for loop variable increments, the property values for the objects that are stored inside the list change as well.

Here is the code sample

 List<IEnumerable<string>> data = new List<IEnumerable<string>>()
             {
                 new List<string> { "Item1", "Item2", "Item3", "Item4" },
                 new List<string> { "Item5", "Item6", "Item7", "item8" },
                 new List<string> { "Item9", "Item10", "Item11", "Item12" }
             };
    
             List<IEnumerable<Item>> itemRows = new List<IEnumerable<Item>>();
    
             for (int i = 0; i < data.Count; i++)
             {
                 var columns = data.ElementAt(i);
    
                 var items = columns.Select((value, index) => new { Value = value, Index = index })
                     .Select(x => new Item { Index = i });
    
                 itemRows.Add(items);
    
                 Console.WriteLine($"Value of 'i': {i}");
                 Console.WriteLine(string.Join("\n", itemRows.Select(x => string.Join("-", x.Select(y => y.Index)))) + "\n");
             }

And here is what it produces:

Value of 'i': 0
0-0-0-0

Value of 'i': 1
1-1-1-1
1-1-1-1

Value of 'i': 2
2-2-2-2
2-2-2-2
2-2-2-2

If I call .ToList() on the IEnumerable or even assign 'i' to a new variable and then use that, the behavior goes away:

 List<IEnumerable<string>> data = new List<IEnumerable<string>>()
             {
                 new List<string> { "Item1", "Item2", "Item3", "Item4" },
                 new List<string> { "Item5", "Item6", "Item7", "item8" },
                 new List<string> { "Item9", "Item10", "Item11", "Item12" }
             };
    
             List<IEnumerable<Item>> itemRows = new List<IEnumerable<Item>>();
    
             for (int i = 0; i < data.Count; i++)
             {
                 var columns = data.ElementAt(i);
    
                 var items = columns.Select((value, index) => new { Value = value, Index = index })
                     .Select(x => new Item { Index = i });
    
                 itemRows.Add(items);
    
                 Console.WriteLine($"Value of 'i': {i}");
                 Console.WriteLine(string.Join("\n", itemRows.Select(x => string.Join("-", x.Select(y => y.Index)))) + "\n");
             }

Why is it only with an IEnumerable and goes when to calling .ToList()

dotnet-csharp
· 3
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

The .ToList() method executes the LINQ expression. The way the code is written the last value of i is what's used when the query is actually executed.

1 Vote 1 ·

Instead of ToList, check the effect of this change too:

 . . .
 int k = i;
 var items = columns.Select( ( value, index ) => new { Value = value, Index = index } )
     .Select( x => new Item { Index = k } );
 . . .

1 Vote 1 ·

Thank you. I had mentioned that already in my post

"If I call .ToList() on the IEnumerable or even assign 'i' to a new variable and then use that, the behavior goes away:"

0 Votes 0 ·

1 Answer

Paul-5034 avatar image
0 Votes"
Paul-5034 answered Paul-5034 edited

This is because IEnumerables are evaluated only when you perform an operation on them that forces them to become concreted. You can store a collection of IEnumerable, all of which have .Select() operations that use a value from the current scope (in this case i) and it may be different depending on what value it holds at the point that it's evaluated.

Every iteration of your loop ends with a call to string.Join which forces what's in itemRows to become concrete and evaluate the value of i, and that includes all previous entries in itemRows, so the value of i will just be what it is at that point in time for all IEnumerables in the list. This is why every row refers to the current value of i and not the value that i had when the .Select() operation was declared.

If you .ToList() on items before adding to itemRows then you'll have a List<List<Item>> that you'll be covariant-ly referring to by List<IEnumerable<Item>>. This means that the .Select() on items is evaluated/concreted once before adding to itemRows, rather than on every call to string.Join.

· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

So when you say evaluated/concreted - does this have to do with deferred execution?

0 Votes 0 ·
Paul-5034 avatar image Paul-5034 DemitriusWheelwright-4098 ·

Precisely, string.Join enumerates the IEnumerable and that forces the execution, however even though it's executing it you still have the unexecuted IEnumerable in your list that can be re-evaluated, and each time may contain different values depending on what variables you're "capturing" in your .Select() call.

Correction: Meant to type string.Join not string.Format !

0 Votes 0 ·