Deferred execution
The conveyor belt from the previous lessons is trickier than it looks. As long as nobody takes a box at the end of the belt, it stands still. Reach out a hand, and the belt moves by exactly one box, and every station handles only that box.
A LINQ query works the same way. A line with Where or Select computes nothing: it only assembles the belt. See what follows from that:
List<int> nums = [1, 2, 3];
var big = nums.Where(n => n > 1);
nums.Add(4);
Console.WriteLine(string.Join(" ", big)); // 2 3 4The number 4 was added after the line with Where, and yet the query saw it. When the line with Where ran, nothing was filtered: the query remembered the list and the lambda. The filtering happened in the last line, when string.Join started to go through the query item by item.
A query runs when it is enumerated
This behavior is called deferred execution: a LINQ query runs not where it is written but when its result is enumerated, that is, gone through item by item. A query is enumerated by foreach, string.Join, methods such as Count() and First(), and also ToList() and ToArray().
In the program below, the lambda for Select prints a line on every call. The entry → nums.Select(…) next to doubled means the variable leads to a query, not to ready numbers. Press "Next step" and watch when the Select lines appear.
1List<int> nums = [1, 2];2var doubled = nums.Select(n =>3{4 Console.WriteLine($"Select {n}");5 return n * 2;6});7nums.Add(3);8Console.WriteLine("Query ready");9foreach (int d in doubled)10{11 Console.WriteLine(d);12}
Main creates a list of two numbers.
The step-by-step run shows three things:
Query readyprints beforeSelect 1,Select 2andSelect 3: the line withSelectdid not call the lambda;- the items come one at a time: the lambda handled
1,foreachprinted2, and only then did the second item get its turn; - the number
3, added after the query, went through the lambda too: by the time of the loop it was already in the list.
Every enumeration starts over
A query does not remember its result. Each time it is enumerated, it walks the list again and calls the lambdas again. If the program above enumerated doubled with a second foreach, the lines Select 1, Select 2 and Select 3 would print once more.
Here the conveyor belt analogy ends. A box taken off the belt is gone for good. A query, though, can be enumerated as many times as you like, and each time the belt starts from the beginning.
A snapshot with ToList
To fix the result, the query is run at once:
List<int> nums = [1, 2, 3];
var big = nums.Where(n => n > 1);
List<int> snapshot = big.ToList();
nums.Add(4);
Console.WriteLine(string.Join(" ", big)); // 2 3 4
Console.WriteLine(string.Join(" ", snapshot)); // 2 3ToList() enumerates the query at once and puts the result into a new list. That is a snapshot: after nums.Add(4) the query big sees 4, while snapshot stays as it was.
Call ToList() or ToArray() when:
- you need the result as of now, and the source list will change later;
- the result will be enumerated several times, and the lambdas must not run again.
Deferred execution in interviews
Interviewers like to ask this: when does a LINQ query run? The answer has two parts:
Where,SelectandOrderByonly build the query, and it runs when it is enumerated;Count,Sum,First,Any,ToListandToArraystart the enumeration at once, so the query runs in that same line.
Deferred execution is the last theme of the course. You can practice answering such questions in the IT Abyss interview simulator.
Common mistake
The list was changed inside a foreach that enumerates a query over that same list:
List<int> nums = [1, 2, 3];
foreach (int n in nums.Where(x => x > 1))
{
nums.Add(n * 10); // ✗
}Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.The program compiles, but it stops with an exception. The query is deferred, so Where reads nums right during the loop, and the list was changed in the middle of the enumeration. It is the same error as changing a list inside a foreach over the list itself.
The fix is a snapshot: foreach (int n in nums.Where(x => x > 1).ToList()). The loop goes through a ready list, and 20 and 30 are added to nums without an error.
Key points
- A LINQ query runs not where it is written but when its result is enumerated.
- A query sees the list as it is at the moment of enumeration, and it runs again on every enumeration.
ToList()andToArray()run the query at once and keep a snapshot that does not follow the list's changes.- You cannot change a list while a query over it is being enumerated: the program stops with an
InvalidOperationException.