tags:

views:

135

answers:

4

Gday All,

I came across an interesting question today where I have two methods that, at a quick glance, both do the same thing. That is return an IEnumerable of Foo objects.

I have defined them below as List1 and List2:

public class Foo
{
    public int ID { get; set; }
    public bool Enabled { get; set;}    
}
public static class Data
{
    public static IEnumerable<Foo> List1
    {
        get
        {
            return new List<Foo>
                       {
                           new Foo {ID = 1, Enabled = true},
                           new Foo {ID = 2, Enabled = true},
                           new Foo {ID = 3, Enabled = true}
                       };
        }
    }
    public static IEnumerable<Foo> List2
    {
        get
        {
            yield return new Foo {ID = 1, Enabled = true};
            yield return new Foo {ID = 2, Enabled = true};
            yield return new Foo {ID = 3, Enabled = true};
        }
    }
}

Now consider the following tests:

IEnumerable<Foo> listOne = Data.List1;
listOne.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listOne.ElementAt(1).Enabled);
Assert.AreEqual(false, listOne.ToList()[1].Enabled);  

IEnumerable<Foo> listTwo = Data.List2;
listTwo.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listTwo.ElementAt(1).Enabled);
Assert.AreEqual(false, listTwo.ToList()[1].Enabled);

These two methods seem to do the "same" thing.

Why do the second assertions in the test code fail?
Why is listTwo's second "Foo" item not getting set to false when it is in listOne?

NOTE: I'm after an explanation of why this is allowed to happen and what the differences in the two are. Not how to fix the second assertion as I know that if I add a ToList call to List2 it will work.

Cheers,

Michael

+5  A: 

The first block of code builds the items once and returns a list with the items.

The second block of code builds those items each time the IEnumerable is walked through.

This means that the second and third line of the first block operate on the same object instance. The second block's second and third line operate on different instances of Foo (new instances are created as you iterate through).

The best way to see this would be to set breakpoints in the methods and run this code under the debugger. The first version will only hit the breakpoint once. The second version will hit it twice, once during the .Where() call, and once during the .ElementAt call. (edit: with the modified code, it will also hit the breakpoint a third time, during the ToList() call.)

The thing to remember here is that an iterator method (ie. it uses yield return) will be run every time the enumerator is iterated through, not just when the initial return value is constructed.

Jonathan
+1  A: 

listTwo is an iterator - a state machine.

ElementAt must start at the beginning of the iterator to correctly get the i-th index in the IEnumerable (whether or not it is an iterator state machine or a true IEnumerable instance), and as such, listTwo will be reinitialized with the default values of Enabled = true for all three items.

Jeff Meatball Yang
A: 

Suggestion: Compile the code and open with reflector. Yield is a syntactical suger. You would be able to see the code logic difference in the code your wrote and the code generated for the yield keyword. Both are not the same.

Shafqat Ahmed
+2  A: 

Those are definitely not the same thing.

The first builds and returns a list the moment you call it, and you can cast it back to list and list-y things with it if you want, including add or remove items, and once you've put the results in a variable you're acting on that single set of results. Calling the function would produce another set of results, but re-using the result of a single call acts on the same objects.

The second builds an IEnumerable. You can enumerate it, but you can't treat it as a list without first calling .ToList() on it. In fact, calling the method doesn't do anything until you actually iterate over it. Consider:

var fooList = Data.List2().Where(f => f.ID > 1);
// NO foo objects have been created yet.
foreach (var foo in fooList)
{
   // a new Foo object is created, but NOT until it's actually used here
   Console.WriteLine(foo.Enabled.ToString());  
}

Note that the code above will create the first (unused) Foo instance, but not until entering the foreach loop. So the items aren't actually created until called for. But that means every time you call for them, you're building a new set of items.

Joel Coehoorn