Tim Schmelter
Tim Schmelter

Reputation: 460148

For-Loop and LINQ's deferred execution don't play well together

The title suggests that i've already an idea what's going on, but i cannot explain it. I've tried to order a List<string[]> dynamically by each "column", beginning with the first and ending with the minimum Length of all arrays.

So in this sample it is 2, because the last string[] has only two elements:

List<string[]> someValues = new List<string[]>();
someValues.Add(new[] { "c", "3", "b" });
someValues.Add(new[] { "a", "1", "d" });
someValues.Add(new[] { "d", "4", "a" });
someValues.Add(new[] { "b", "2" });

Now i've tried to order all by the first and second column. I could do it statically in this way:

someValues = someValues
    .OrderBy(t => t[0])
    .ThenBy(t => t[1])
    .ToList();

But if i don't know the number of "columns" i could use this loop(that's what I thought):

int minDim = someValues.Min(t => t.GetLength(0));  // 2
IOrderedEnumerable<string[]> orderedValues = someValues.OrderBy(t => t[0]);
for (int i = 1; i < minDim; i++)
{
    orderedValues = orderedValues.ThenBy(t => t[i]);
}
someValues = orderedValues.ToList();  // IndexOutOfRangeException 

But that doesn't work, it fails with an IndexOutOfRangeException at the last line. The debugger tells me that i is 2 at that time, so the for-loop condition seems to be ignored, i is already == minDim.

Why is that so? What is the correct way for this?

Upvotes: 5

Views: 319

Answers (2)

Dirk
Dirk

Reputation: 10958

It's the same problem as lots of people had with foreach loops pre C# 5.

orderedValues = orderedValues.ThenBy(t => t[i]);

The value of i will not be evaluated until you call .ToList() at which point it is 2 since that's the exit condition of the for loop.

You can introduce a new local variable inside the for-loop to fix it:

for (int i = 1; i < minDim; i++)
{
    var tmp = i;
    orderedValues = orderedValues.ThenBy(t => t[tmp]);
}

For more information you could take a look at Eric Lippert's blog post about Closing over the loop variable considered harmful.

Upvotes: 7

D Stanley
D Stanley

Reputation: 152566

This is probably happening because the value of i is not closed within the loop - when the loop exits, i will have a value of 2 and then t[i] will be evaluated because of deferred execution.

One solution is to create a closing variable within the loop:

int minDim = someValues.Min(t => t.GetLength(0));  // 2
IOrderedEnumerable<string[]> orderedValues = someValues.OrderBy(t => t[0]);
for (int i = 1; i < minDim; i++)
{
    var x = i;
    orderedValues = orderedValues.ThenBy(t => t[x]);
}
someValues = orderedValues.ToList(); 

Upvotes: 6

Related Questions