Chris
Chris

Reputation: 27599

Iterator issue on yield IEnumerable

I wrote a program designed to create a randomish list of numbers from a given starting point. It was a quick a dirty thing but I found an interesting effect when playing with it that I don't quite understand.

void Main()
{
    List<int> foo = new List<int>(){1,2,3};
    IEnumerable<int> bar = GetNumbers(foo);
    for (int i = 1; i < 3; i++)
    {
        foo = new List<int>(){1,2,3};
        var wibble = GetNumbers(foo);
        bar = bar.Concat(wibble);
    }
    Iterate(bar);
    Iterate(bar);
}

public void Iterate(IEnumerable<int> numbers)
{
    Console.WriteLine("iterating");
    foreach(int number in numbers)
    {
        Console.WriteLine(number);
    }
}

public IEnumerable<int> GetNumbers(List<int> input)
{
    //This function originally did more but this is a cutdown version for testing.
    while (input.Count>0)
    {
        int returnvalue = input[0];
        input.Remove(input[0]);
        yield return returnvalue;
    }
}

The output of runing this is:

iterating
1
2
3
1
2
3
1
2
3
iterating

That is to say the second time I iterate through bar immediately after it is empty.

I assume this is something to do with the fact that the first time I iterate that it empties the lists that are being used to generate the list and subsequently it is using these same lists that are now empty to iterate.

My confusion is on why this is happening? Why do my IEnumerables not start from their default state each time I enumerate over them? Can somebody explain what exactly I'm doing here?

And to be clear I know that I can solve this problem by adding a .ToList() to my call to GetNumbers() which forces immediate evaluation and storage of the results.

Upvotes: 1

Views: 118

Answers (3)

Vlad
Vlad

Reputation: 35584

Well, the lazy evaluation is what hit you. You see, when you create a yield return-style method, it's not executed immediately upon call. It'll be however executed as soon as you iterate over the sequence.

So, this means that the list won't be cleared during GetNumbers, but only during Iterate. In fact, the whole body of the function GetNumbers will be executed only during Iterate.

You problem is that you made your IEnumersbles depend not only on inner state, but on outer state as well. That outer state is the content of foo lists.

So, the all the lists are filled until you Iterate the first time. (The IEnumerable created by GetNumbers holds a reference to them, so the fact that you overwrite foo doesn't matter.) All the three are emptied during the first Iterate. Next, the next iteration starts with the same inner state, but changed outer state, giving different result.

I'd like to notice, that mutation and depending on outer state is generally frowned upon in functional programming style. The LINQ is actually a step toward functional programming, so it's a good idea to follow the FP's rules. So you could do better with just not removing the items from input in GetNumbers.

Upvotes: 1

Daniel Hilgarth
Daniel Hilgarth

Reputation: 174289

Your observation can be reproduced with this shorter version of the main method:

void Main() 
{ 
    List<int> foo = new List<int>(){1,2,3}; 
    IEnumerable<int> bar = GetNumbers(foo); 
    Console.WriteLine(foo.Count); // prints 3
    Iterate(bar); 
    Console.WriteLine(foo.Count); // prints 0
    Iterate(bar); 
} 

What happens is the following:

When you call GetNumbers it isn't really being executed. It will only be executed when you iterate over the result. You can verify this by putting Console.WriteLine(foo.Count); between the call to GetNumbers and Iterate.
On the first call to Iterate, GetNumbers is executed and empties foo. On the second call to Iterate, GetNumbers is executed again, but now foo is empty, so there is nothing left to return.

Upvotes: 3

user743382
user743382

Reputation:

Your iterator does start from its initial state. However, it modifies the list it's reading from, and once the list is cleared, your iterator doesn't have anything left to do. Basically, consider

var list = new List<int> { 1, 2, 3 };
var enumerable = list.Where(i => i != 2);
foreach (var item in enumerable)
    Console.WriteLine(item);
list.Clear();
foreach (var item in enumerable)
    Console.WriteLine(item);

enumerable doesn't get changed by list.Clear();, but the results it gives do.

Upvotes: 6

Related Questions