Why does calling Enumerable.First() appear to return a copy of the first item in the enumeration

418 views Asked by At

Er, not quite sure how to phrase this but..

Given an IEnumerable created using yield return, containing three instances of a class, why does calling .First() seem to return a 'copy' of the first instance?

See the following code;

public class Thing
    {
        public bool Updated { get; set; }

        public string Name { get; private set; }

        public Thing(string name)
        {
            Name = name;
        }

        public override string ToString()
        {
            return string.Format("{0} updated {1} {2}", Name, Updated, GetHashCode());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("IEnumerable<Thing>");
            var enumerableThings = GetThings();
            var firstThing = enumerableThings.First();
            firstThing.Updated = true;
            Console.WriteLine("Updated {0}", firstThing);
            foreach (var t in enumerableThings)
                Console.WriteLine(t);

            Console.WriteLine("IList<Thing>");
            var thingList = GetThings().ToList();
            var thing1 = thingList.First();
            thing1.Updated = true;
            Console.WriteLine("Updated {0}", thing1);
            foreach (var t in thingList)
                Console.WriteLine(t);

            Console.ReadLine();
        }

        private static IEnumerable<Thing> GetThings()
        {
            for (int i = 1; i <= 3; i++)
            {
                yield return new Thing(string.Format("thing {0}", i));
            }
        }
    }
}

running this produces the following output;

IEnumerable<Thing>
Updated thing 1 updated True 37121646
thing 1 updated False 45592480
thing 2 updated False 57352375
thing 3 updated False 2637164
IList<Thing>
Updated thing 1 updated True 41014879
thing 1 updated True 41014879
thing 2 updated False 3888474
thing 3 updated False 25209742

but I would expect the IList and IEnmerable to behave the same and output like this...

IEnumerable<Thing>
Updated thing 1 updated True 45592480
thing 1 updated False 45592480
thing 2 updated False 57352375
thing 3 updated False 2637164
IList<Thing>
Updated thing 1 updated True 41014879
thing 1 updated True 41014879
thing 2 updated False 3888474
thing 3 updated False 25209742

What am I missing?!

4

There are 4 answers

1
Shadow Wizard Love Zelda On BEST ANSWER

The method GetThings does not return real collection. It returns a "recipe" how to "cook" a collection, and it's "cooked" only when you ask to iterate it. That's the magic of yield.

So every time you call .First() the loop is running and indeed, new instance is created.

0
Dan Bryant On

The IEnumerable constructed by 'yield return' only produces values when it's enumerated and you enumerate it twice in the first case. You're actually creating a completely separate set of Things when you enumerate it the second time.

Yield return is basically a shortcut for code that generates a state machine, which, when enumerated, progresses through the code to yield the enumerated results. The results themselves are not saved anywhere until you do something with them, like put them into a List.

0
Joe White On

Iterators (any method that uses yield return) are lazy-evaluated when you iterate them. That means your method body doesn't execute when you call it -- it only executes when you enumerate the resulting IEnumerable<T>, via a call to foreach or some such. And it executes each time you foreach.

Since .First() has to enumerate the IEnumerable<T> (as that's the only way to get elements out of it), your method body gets re-run each time you call .First().

The usual solution is to force the iterator to run at some point when you're ready, by calling .ToList() or .ToArray(). That will give you a List<T> or an array, which will no longer change when you iterate it.

0
wageoghe On

Your IEnumerable implementation (GetThings) is returning new items each time you iterate. So, in the foreach of the IEnumerable, each Thing is newly created. When you add ToList to your IEnumerable, the list does indeed contain a "copy" of each item that is yielded from the IEnumerable in the sense that the "things" that are created by your IEnumerable are saved into the list. Subsequent iterations over the list of "things" will always yield the same set of "things". Subsequent iterations over the IEnumerable will always yield a new set of "things".