Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Item from IEnumerable changed inside foreach, but change not saved to collection

Tags:

c#

ienumerable

I had an interesting problem today, this is a reproduction:

class TestListAndEnumerable
{
    public static void Test()
    {
        bool b1 = GetBool1(); // returns false
        bool b2 = GetBool2(); // returns true
    }

    private static bool GetBool1()
    {
        IEnumerable<BoolContainer> list = new List<bool> { false }.Select(x => new BoolContainer { Value = x });

        foreach (BoolContainer item in list)
        {
            item.Value = true;
        }

        return list.Select(x => x.Value).First();
    }

    private static bool GetBool2()
    {
        List<BoolContainer> list = new List<bool> { false }.Select(x => new BoolContainer { Value = x }).ToList();

        foreach (BoolContainer item in list)
        {
            item.Value = true;
        }

        return list.Select(x => x.Value).First();
    }

    private class BoolContainer
    {
        public bool Value { get; set; }
    }
}

Inside GetBool1(), changing the value of item has no effect on the list variable.
GetBool2() works as expected.

Anyone know why this is happening?

[Regarding the duplicate tag]

I think it IS fundamentally the same problem as in this question, but the question here is more concise and to the point so I'll leave it.

like image 290
mrzli Avatar asked Dec 08 '22 03:12

mrzli


1 Answers

This is a matter of LINQ's lazy evaluation. Each time you iterate over list in GetBool1(), you're creating a new sequence of values. Changing the Value property in one of those objects doesn't change anything else.

Compare that with GetBool2(), where you're got a call to ToList(), which means you're creating a List<T> with a sequence of objects in. Now each time you iterate over that list, you'll get references to the same objects... so if you modify the value within one of those objects, you'll see that next time.

If you change your Select argument to

x => { Console.WriteLine("Selecting!"); return new BoolContainer { Value = x } };

... then add appropriate extra Console.WriteLine calls so you can see when it's taking place, you'll see that the delegate is never called in GetBool2() after the call to ToList(), but in GetBool1() it will be called each time you iterate over the sequence.

like image 177
Jon Skeet Avatar answered May 25 '23 23:05

Jon Skeet