1
IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
   charQuery1 = charQuery1.Where(c => c != vowels[i]);
foreach (char c in charQuery1) Console.Write(c);

This is will throw IndexOutofRange Exception. Compiler scopes the for loop variable "i" as like declared outside. Can someone dissect this and make me more comfortable to understand? Debugging did not help me much here. I am trying to understand the compiler role here.

Ziv
  • 95
  • 7
  • 1
    not answering your question but this would be the correct way: `charQuery1 = charQuery1.Except(vowels)` – Tim Schmelter Dec 05 '21 at 20:07
  • Does this answer your question? Sorry wrong link https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp – Charlieface Dec 05 '21 at 20:33
  • You need to copy the loop variable inside the loop – Charlieface Dec 05 '21 at 20:36
  • 1
    This answer helps to understand the issue: https://stackoverflow.com/a/8993720/284240 The loop variable `i` will be transferred to a field in a class. It's value is increased in the loop until 5, the foreach will execute the query with the final value 5. – Tim Schmelter Dec 05 '21 at 20:43

2 Answers2

2

Linq functions such as Where create an IEnumerable that will be lazily evaluated.

This means the result of the Where function will not be produced when the Where function is being invoked. The result of the Where function is being produced when the IEnumerable returned by Where is being iterated.

In your example code, the results of the Where functions are iterated in the foreach loop at the end of your code:

foreach (char c in charQuery1) Console.Write(c);

This is where the enumerabled returned by the Where functions are being iterated, causing the predicate delegates provided to the Where functions to be executed.

In your case, those predicate delegate are anonymous functions in the form of c => c != vowels[i], which close over the variable i.

Once again, those anonymous functions c => c != vowels[i] are invoked only when the foreach loop at the end of your code is being executed.

So, what is the value of i when the foreach loop is being executed? It's the last value it has when the for loop exits. In other words, the value of i is equal to vowels.Length.

That leads to the predicate delegate to be equivalent to c => c != vowels[vowels.Length]. Obviously, vowels[vowels.Length] will cause an IndexOutOfRangeException.

How to fix it?

The naive approach would be to not let your predicates close over the i counter variable of the for loop. Instead, let it close over a variable that is scoped inside the for body:

for (int i = 0; i < vowels.Length; i++)
{
    var local_i = i;
    charQuery1 = charQuery1.Where(c => c != vowels[local_i]);
}

But as the comment by Tim Schmelter points out, the more elegant and much shorter solution would be to simply use Linq's Except method:

var charQueryWithoutVowels = charQuery1.Except(vowels);
  • Very detail and clear explanation, but it give to possibility to look on closure Lambda from other point, thanks it help me. But another elegant solution to use the foreach vs for ` foreach (var itemChar in vowels) charQuery1 = charQuery1.Where(c => c != itemChar);` – Denis Sivtsov Feb 07 '23 at 11:40
0

The predicate you pass into Where (which is c != vowels[i]) is executed in deferred way when you enumerate charQuery1 in last part of the code (foreach (char c in charQuery1)). By that moment variable i has the value which was the last one in the loop for (int i = 0; i < vowels.Length; i++) which is '5'. So you captured the value which is modified from inner closure. You must remember that LINQ expressions have deferred execution and don't execute when you declare them but when you enumerate them.

To avoid this problem you should capture loop index into temporary variable withing the loop scope like this.

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
{
   int index = i; // Temporary index variable. Should be declared inside the scope!
   charQuery1 = charQuery1.Where(c => c != vowels[index]);
}
foreach (char c in charQuery1) Console.Write(c);

UPD: Fully deferred behavior:

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
Enumerable
    .Range(0, vowels.Length)
    .ForEach(i =>
    {
        charQuery1 = charQuery1.Where(c => c != vowels[i]);
    });
foreach (char c in charQuery1) Console.Write(c);

But even further simplification will give you this:

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";

charQuery1 = Enumerable
    .Range(0, vowels.Length)
    .Aggregate(charQuery1, (cq, i) => cq.Where(c => c != vowels[i]));

foreach (char c in charQuery1) Console.Write(c);

Cool, isn't it?

zhe
  • 2,358
  • 2
  • 15
  • 12
  • Can you explain deferred execution in little more detail? When I enumerate and print, doesn't it goes to for loop again? if i=5 then it should never execute. right? – Ziv Dec 05 '21 at 21:00
  • @Jeevan The `for` loop is executed immediately but LINQ expressions are executed only when enumerated. In your example you have mix of these two behaviors and that's the cause of the problem. Id suggest to stick with complete deferred behavior and replace your first loop with this: `Enumerable .Range(0, vowels.Length) .ForEach(i => { charQuery1 = charQuery1.Where(c => c != vowels[i]); });` – zhe Dec 05 '21 at 21:34
  • 1
    Thanks! That's what I was confused with. Immediate execution of for loop and deferred execution of delegate. Now I am clear. TY – Ziv Dec 05 '21 at 21:53
  • It will work but not so elegant as more simple solution ` foreach (var itemChar in vowels) charQuery1 = charQuery1.Where(c => c != itemChar);`. The term "Fully deferred behavior" is strange because "fully" or "not fully" change nothing – Denis Sivtsov Feb 07 '23 at 12:08