Skip to content
Theory · Lambdas and LINQ · lesson 9 of 10

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:

C#
List<int> nums = [1, 2, 3];
var big = nums.Where(n => n > 1);
nums.Add(4);
Console.WriteLine(string.Join(" ", big)); // 2 3 4

The 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.

LiveWhen Select does its workstep 1 of 13 · frames: 1

Main creates a list of two numbers.

The lambda runs only when foreach asks for the next item

The step-by-step run shows three things:

  • Query ready prints before Select 1, Select 2 and Select 3: the line with Select did not call the lambda;
  • the items come one at a time: the lambda handled 1, foreach printed 2, 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:

C#
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 3

ToList() 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, Select and OrderBy only build the query, and it runs when it is enumerated;
  • Count, Sum, First, Any, ToList and ToArray start 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:

C#
List<int> nums = [1, 2, 3];
foreach (int n in nums.Where(x => x > 1))
{
    nums.Add(n * 10); // ✗
}
TEXT
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() and ToArray() 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.