1

I have a collection of objects, and want to modify a property on an object in that collection. If I have a single object, my ChangeStuff method works fine, and the object is modified when returning from the method. (First 4 lines in Main)

However, when iterating through the collection, I guess I'm missing something, as my changed values seem to be stuck in the scope of the foreach loop.

I shouldn't have to pass the objects by ref (or using an out parameter), since I'm not returning a new value.

Sorry for the big code chunk, but it's about as simplified as I can make it and still demonstrate my issue.

    class foobar
    {
        public string string1;
        public string string2;
    }

    static void Main(string[] args)
    {
        /***** THIS WORKS!! *****/
        foobar singleFb = new foobar { string1 = "foo2", string2 = "bar2" };
        ChangeStuff(singleFb);
        Console.WriteLine(singleFb.string1 + ", " + singleFb.string2);

        Console.ReadLine(); //pause to read output

        /***** THIS DOESN'T WORK!! *****/
        List<foobar> myList = new List<foobar> { new foobar {string1 = "foo1", string2 = "bar1"}, new foobar {string1 = "foo2", string2 = "bar2"}, new foobar {string1 = "something else", string2 = "something else again"} };
        IEnumerable<foobar> fbs = myList.Where(x => x.string1.StartsWith("foo"));

        ChangeStuff(fbs);

        foreach (foobar fb in fbs)
        {
            Console.WriteLine(fb.string1 + ", " + fb.string2);
        }
        Console.ReadLine(); //pause to read output
    }

    static void ChangeStuff(IEnumerable<foobar> fbs)
    {
        foreach (foobar fb in fbs)
        {
            ChangeStuff(fb);
        }
    }

    static void ChangeStuff(foobar fb)
    {
        if (fb.string1.Contains("2"))
            fb.string1 = "changed!";
    }
}

What do I need to change in order to modify an object in a collection?


Edit: Also, just noticed that my collection is actually completely missing "foo2" when it comes back... Weird. I'm actually using IQueryable in my application, and haven't experienced this problem. ie. I have all the objects, they just aren't correctly modified. Not sure what's going on here...


Edit 2: Thank you for your answers, it makes perfect sense now. If I change my ChangeStuff method as follows, it works as I would expect:
    static void ChangeStuff(foobar fb)
    {
        if (fb.string2.Contains("2"))
            fb.string2 = "changed!";
    }
Community
  • 1
  • 1
sǝɯɐſ
  • 2,470
  • 4
  • 33
  • 47

2 Answers2

7

The result you are seeing is due to the lazy-load nature of IEnumerable<T>. The second time you enumerate over "fbs", the property "string1" has changed so that it no longer matches the Where predicate.

If you enumerate the IEnumerable immediately and store the result in a concrete list (eg, by using ToList() or ToArray(), then the second enumeration will show the result you expect:

IEnumerable<foobar> fbs = myList.Where(x => x.string1.StartsWith("foo")).ToList();

Here's what happens in the original version of the code:

List<foobar> myList = new List<foobar> { new foobar { string1 = "foo1", string2 = "bar1" }, new foobar { string1 = "foo2", string2 = "bar2" }, new foobar { string1 = "something else", string2 = "something else again" } };
IEnumerable<foobar> fbs = myList.Where(x => x.string1.StartsWith("foo"));

// here the enumeration yields objects #1 and #2
// object #2 has its "string1" property modified to "changed!"
ChangeStuff(fbs);

// here the enumeration is re-evaluated, and now object #2 no longer matches the predicate
// only object #1 ("foo1") is output
foreach (foobar s in fbs)
{
    Console.WriteLine(s.string1);
}
McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • Thanks! Makes perfect sense (see Edit2). I assume this works the same way with IQueryable - when printing the properties, it would reload from the db, and my previous edits would be lost? – sǝɯɐſ Oct 21 '14 at 21:19
  • I'm not sure if you changes would be wiped out (that is to say, I think maybe the data context would still hold your changes, and they would be submitted to the DB when you call "SubmitChanges"?). But yes, `IQueryable` is similar to `IEnumerable`, in that it will goes to the source each time, unless you first realize it in a concrete list. – McGarnagle Oct 21 '14 at 21:23
5

The most important thing to remember when dealing with LINQ is that the methods don't return the results of a query, they return an object that represents the query itself. It is when the sequence is enumerated that the query is executed and the items returned.

fbs is a query to get all of the items where the first string starts with foo. It is not a collection of the first two items, even though those are the items that would be returned if you execute that query when you first defined it.

The first time you execute the query and iterate the results you get back two items and try to change them both, only one actually gets changed by ChangeStuff. You then execute the query again to print the results, but now the changed item doesn't meet the query's criteria, and so isn't returned. Only the first item, that wasn't changed, is returned. If you iterate the list, and not your query, you'll see your changed item.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Ahhh... that makes sense. Sounds like my best option then is to convert .ToList() before editing the collection? – sǝɯɐſ Oct 21 '14 at 21:03
  • @sǝɯɐſ That would work, but in this case printing the results should probably just print out the whole list. That would be the correct test in my eyes. Regardless, the point is that you're just not correctly observing the change that is indeed being made. – Servy Oct 21 '14 at 21:04
  • Thanks for a great answer, and I like the point in italics, but I'm going to have to give the points to @McGarnagle, as his code example made it a little clearer for me. – sǝɯɐſ Oct 21 '14 at 21:26