tags:

views:

133

answers:

5

Hi, I know a lot about C# but this one is stumping me and Google isn't helping.

I have an IEnumerable range of objects. I want to set a property on the first one. I do so, but when I enumerate over the range of objects after the modification, I don't see my change.

Here's a good example of the problem:

    public static void GenericCollectionModifier()
    {
        // 1, 2, 3, 4... 10
        var range = Enumerable.Range(1, 10);

        // Convert range into SubItem classes
        var items = range.Select(i => new SubItem() {Name = "foo", MagicNumber = i});

        Write(items);  // Expect to output 1,2,3,4,5,6,7,8,9,10

        // Make a change
        items.First().MagicNumber = 42;

        Write(items);  // Expect to output 42,2,3,4,5,6,7,8,9,10
        // Actual output: 1,2,3,4,5,6,7,8,9,10
    }

    public static void Write(IEnumerable<SubItem> items)
    {
        Console.WriteLine(string.Join(", ", items.Select(item => item.MagicNumber.ToString()).ToArray()));
    }

    public class SubItem
    {
       public string Name;
       public int MagicNumber;
    }

What aspect of C# stops my "MagicNumber = 42" change from being output? Is there a way I can get my change to "stick" without doing some funky converting to List<> or array?

Thanks! -Mike

A: 

I suspect something going in the background. Most likely due to the fact the IEnumerables can only be iterated once.

Does it work if you add a 'ToList()' after the call to Select() when assigning to 'items'?

leppie
Yes, if I convert it to a List it works as expected. I'm hoping to avoid that though.
Mike
IEnumerables cannot only be iterated once. They are lazy evaluating so that each time you iterate you may get a different result. Adding ToList() will fix the problem, however.
Simon Steele
A: 

The only thing I can think of is that items.First() passes a copy of SubItem to your instead of the reference, so when you set it the change isn't carried through.

I have to assume it has something to do with IQueryable only being able to be iterated once. You may want to try changing this:

// Convert range into SubItem classes
var items = range.Select(i => new SubItem() {Name = "foo", MagicNumber = i});

to

// Convert range into SubItem classes
var items = range.Select(i => new SubItem() {Name = "foo", MagicNumber = i}).ToList();

And see if there are any different results.

Nick Berardi
+6  A: 

When you call First() it enumerates over the result of this bit of code:

Select(i => new SubItem() {Name = "foo", MagicNumber = i});

Note that the Select is a lazy enumerator, meaning that it only does the select when you ask for an item from it (and does it every time you ask it). The results are not stored anywhere, so when you call items.First() you get a new SubItem instance. When you then pass items to Write, it gets a whole bunch of new SubItem instances - not the one you got before.

If you want to store the result of your select and modify it, you need to do something like:

var items = range.Select(i => new SubItem() {Name = "foo", MagicNumber = i}).ToList();
Simon Steele
A: 

You can't/shouldn't modify a collection through an enumerator. I'm surprised this doesn't throw an exception.

Will
The collection isn't being modified, the data inside an item is
Mike
A: 

.First() is a method, not a property. It returns a new instance of the object in the first position of your Enumerable.

Aaron Palmer
It does not return a copy, it returns the actual object
Mike