Deferred execution and iterators
Understand when LINQ queries actually run, and write your own lazy sequences with yield return.
12 min read
You will learn
- Explain deferred vs immediate execution
- Avoid multiple-enumeration surprises
- Write iterator methods with yield return
- Work with infinite, lazily generated sequences
A query is a recipe, not a result
This might surprise you:
var numbers = new List<int> { 1, 2, 3 };
var big = numbers.Where(n => n > 1); // nothing is filtered yet!
numbers.Add(10);
Console.WriteLine(string.Join(", ", big)); // 2, 3, 10 — includes the 10 added laterWhere doesn't compute anything. It returns an object that describes the query. The filtering happens only when someone enumerates it — with foreach, ToList(), Count() and so on. This is deferred execution (or lazy evaluation).
Seeing it happen
C# runtime not loaded
Notice two things:
- Nothing runs until
First()is called. - Elements flow through the pipeline one at a time.
First()stops as soon as it has a result —3and4are never checked the first time.
Deferred vs immediate operators
| Deferred (return a query) | Immediate (run the query) |
|---|---|
Where, Select, SelectMany | ToList, ToArray, ToDictionary |
OrderBy, ThenBy, GroupBy, Join | Count, Sum, Average, Min, Max |
Skip, Take, Distinct, Concat | First, Single, Last, Any, All |
Pitfall: multiple enumeration
Every enumeration runs the query again:
var expensive = orders.Where(o => ExpensiveCheck(o));
if (expensive.Any()) // runs the query
Console.WriteLine(expensive.Count()); // runs it again
foreach (var o in expensive) { ... } // and againIf the query is costly — or reads from a database or file — materialize it once:
var expensive = orders.Where(o => ExpensiveCheck(o)).ToList(); // run once, reuse the listWatch out
Deferred execution can also surprise you with when errors happen: an exception inside a Where lambda is thrown when the query is enumerated, maybe far from where it was written.
Writing your own lazy sequences: yield return
You can create deferred sequences yourself with an iterator method: return IEnumerable<T> and use yield return to produce elements one at a time.
IEnumerable<int> CountTo(int max)
{
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"(producing {i})");
yield return i; // hand out one value, then pause here
}
}
foreach (var n in CountTo(3))
Console.WriteLine($"got {n}");
// (producing 1) got 1 (producing 2) got 2 (producing 3) got 3The method doesn't run to completion when called. Each time the consumer asks for the next item, it resumes right after the last yield return. yield break ends the sequence early.
Infinite sequences
Because items are produced on demand, a sequence can be endless — the consumer decides how much to take:
IEnumerable<int> Naturals()
{
int n = 0;
while (true) yield return n++;
}
var firstSquaresOver50 = Naturals()
.Select(n => n * n)
.Where(sq => sq > 50)
.Take(3); // 64, 81, 100 — and the loop stops thereWhy laziness matters
- Efficiency — only the work you need is done;
First()can stop early. - Streaming — process a 10 GB log file line by line (
File.ReadLines) without loading it all into memory. - Composition — build queries in pieces, and add filters conditionally before running them.
In Track 10 you'll see the database version of this: an EF Core query isn't sent to the database until you enumerate it — which is how a whole LINQ pipeline becomes a single SQL statement.
Key takeaways
- Most LINQ operators are deferred: they describe work that runs when enumerated.
- Each enumeration re-runs the query; use
ToList()to run it once and reuse the results. yield returncreates lazy iterator methods that resume where they left off.- Lazy sequences can be infinite — the consumer decides how much to take.
Exercise
Write two iterator methods using yield return. Fibonacci produces the endless sequence 0, 1, 1, 2, 3, 5, … (it must be lazy — callers use Take and First on it). Numbered yields each line prefixed with its 1-based position: "1: first", "2: second", …
C# runtime not loaded
Hint 1
Fibonacci: keep two variables a = 0, b = 1 and loop forever with while (true) { yield return a; (a, b) = (b, a + b); } — it's fine, because the caller only pulls what it needs.
Hint 2
Numbered: keep a counter, and foreach line yield return $"{counter}: {line}";
Show solution
Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
foreach (var line in Numbered(new[] { "alpha", "beta" }))
Console.WriteLine(line);
IEnumerable<long> Fibonacci()
{
long a = 0, b = 1;
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}
IEnumerable<string> Numbered(IEnumerable<string> lines)
{
int number = 1;
foreach (var line in lines)
{
yield return $"{number++}: {line}";
}
}Quiz
4 questions · 3 correct to pass1.When does the filtering in `var q = list.Where(x => x > 5);` actually run?
2.`var q = list.Where(x => x > 5); list.Add(10);` — does enumerating q include 10?
3.Which call forces immediate execution?
4.What does `yield return` do inside a method returning IEnumerable<T>?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)