Search code examples
c#linq

What is the relation between the IEnumerable produced by LINQ and the original list?


I have the following lines of code:

var list = new List<Test>() { new Test("Test1"), new Test("Test2") };
var enumerable = list.Where(t => t.Content == "Test1");

Console.WriteLine($"Enumerable count: {enumerable.Count()}");
Console.WriteLine($"List count: {list.Count}");

list.RemoveAll(t => t.Content == "Test1");

Console.WriteLine($"Enumerable count: {enumerable.Count()}");
Console.WriteLine($"List count: {list.Count}");

I would expect the output to be

Enumerable count: 1
List count: 2
Enumerable count: 1
List count: 1

But in fact, the output is

Enumerable count: 1
List count: 2
Enumerable count: 0
List count: 1

Meaning removing the object from the list, also removes it from the IEnumerable. I thought I have a fairly firm grasp on object oriented programming, but this behaviour seems very unexpected to me.

Could anyone explain what's going on behind the scenes? I'll add that if I add .ToList() to the original Where-statement, it all works as I would have expected.

EDIT: The question was closed with a reference to a question about the difference between a list and IEnumerable. That is not at all relevant to what is going on in this question. The issue here was that the IEnumerable was a reference to the LINQ query itself, and not its own collection.


Solution

  • LINQ is lazy and the actual execution is deferred until the moment one of the materializable operations is invoked (Count, foreach, ToList, First, etc.). And the whole enumerable will be enumerated for every such operation. This is very easily observed with side-effect:

    var enumerable = list
        .Where(t =>
        {
            Console.WriteLine("Test deffered: " + t.Content);
            return t.Content == "Test1";
        });
    

    So in you case you will perform the enumeration twice processing the whole list (for every enumerable.Count()) but between the enumerations the list has changed so you see the effect.

    This laziness is actually quite useful in many cases - for example when building queries (for the database via Entity Framework) in dynamic fashion:

    var query = context.Something.AsQueriable();
    
    if(filter.Name is not null)
    {
       query = query.Where(s => s.Name == filter.Name);
    }
    ...
    

    Or reusing the query to fetch different results:

    var query = ...;
    var total = await query.CountAsync();
    
    var page = await query.Skip(page*size).Take(size).ToListAsync();