9

I thought I know everything about IEnumerable<T> but I just met a case that I cannot explain. When we call .Where linq method on a IEnumerable, the execution is deferred until the object is enumerated, isn't it?

So how to explain the sample below :

public class CTest
{
    public CTest(int amount)
    {
        Amount = amount;
    }

    public int Amount { get; set; }

    public override string ToString()
    {
        return $"Amount:{Amount}";
    }

    public static IEnumerable<CTest> GenerateEnumerableTest()
    {
        var tab = new List<int> { 2, 5, 10, 12 };

        return tab.Select(t => new CTest(t));
    }
}

Nothing bad so far! But the following test gives me an unexpected result although my knowledge regarding IEnumerable<T> and .Where linq method :

[TestMethod]
public void TestCSharp()
{
    var tab = CTest.GenerateEnumerableTest();

    foreach (var item in tab.Where(i => i.Amount > 6))
    {
        item.Amount = item.Amount * 2;
    }

    foreach (var t in tab)
    {
        var s = t.ToString();
        Debug.Print(s);
    }
}

No item from tab will be multiplied by 2. The output will be : Amount:2 Amount:5 Amount:10 Amount:12

Does anyone can explain why after enumerating tab, I get the original value. Of course, everything work fine after calling .ToList() just after calling GenerateEnumerableTest() method.

Salah Akbari
  • 39,330
  • 10
  • 79
  • 109

2 Answers2

9
var tab = CTest.GenerateEnumerableTest();

This tab is a LINQ query that generates CTest instances that are initialized from int-values which come from an integer array which will never change. So whenever you ask for this query you will get the "same" instances(with the original Amount).

If you want to "materialize" this query you could use ToList and then change them. Otherwise you are modifying CTest instances that exist only in the first foreach loop. The second loop enumerates other CTest instances with the unmodified Amount.

So the query contains the informations how to get the items, you could also call the method directly:

foreach (var item in CTest.GenerateEnumerableTest().Where(i => i.Amount > 6))
{
    item.Amount = item.Amount * 2;
}

foreach (var t in CTest.GenerateEnumerableTest())
{
   // now you don't expect them to be changed, do you?
}
Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
1

Like many LINQ operations, Select is lazy and use deferred execution so your lambda expression is never being executed, because you're calling Select but never using the results. This is why, everything work fine after calling .ToList() just after calling GenerateEnumerableTest() method:

var tab = CTest.GenerateEnumerableTest().ToList();
Salah Akbari
  • 39,330
  • 10
  • 79
  • 109
  • Sure, the result is used in the `foreach` loops. But each loop will use the query again to request the items from GenerateEnumerableTest. – Tim Schmelter Oct 25 '18 at 09:45