C#
yield return
programming
collections
.NET

C yield return range/collection

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

yield return in C# creates iterator methods that produce elements one at a time without building an entire collection in memory. When iterating over a range or collection, yield return lazily generates each element on demand — the method pauses after each yield and resumes when the next element is requested. This is ideal for large sequences, infinite generators, and transformation pipelines where materializing the full collection would waste memory.

Basic yield return

csharp
1IEnumerable<int> GetNumbers()
2{
3    yield return 1;
4    yield return 2;
5    yield return 3;
6}
7
8foreach (var n in GetNumbers())
9{
10    Console.WriteLine(n);  // 1, 2, 3
11}

The compiler transforms the method into a state machine. Each yield return saves the method's position and returns a value. The next iteration resumes from where it left off.

Generating a Range

csharp
1IEnumerable<int> Range(int start, int count)
2{
3    for (int i = 0; i < count; i++)
4    {
5        yield return start + i;
6    }
7}
8
9foreach (var n in Range(5, 4))
10{
11    Console.WriteLine(n);  // 5, 6, 7, 8
12}

This is equivalent to Enumerable.Range(5, 4) but demonstrates how yield return works with loops. Only one value exists in memory at a time.

Yielding from a Collection

csharp
1IEnumerable<string> FilterLongNames(IEnumerable<string> names)
2{
3    foreach (var name in names)
4    {
5        if (name.Length > 5)
6        {
7            yield return name;
8        }
9    }
10}
11
12var names = new[] { "Alice", "Bob", "Charlie", "David", "Elizabeth" };
13foreach (var name in FilterLongNames(names))
14{
15    Console.WriteLine(name);  // Charlie, Elizabeth
16}

The input collection is iterated lazily — if you break out of the foreach, the remaining items are never processed.

yield return vs Returning a List

csharp
1// Eager — builds entire list in memory first
2List<int> GetSquaresEager(int count)
3{
4    var result = new List<int>();
5    for (int i = 0; i < count; i++)
6    {
7        result.Add(i * i);
8    }
9    return result;  // All items allocated at once
10}
11
12// Lazy — generates one item at a time
13IEnumerable<int> GetSquaresLazy(int count)
14{
15    for (int i = 0; i < count; i++)
16    {
17        yield return i * i;  // One item at a time
18    }
19}
20
21// For 1 million items:
22// Eager: allocates List<int> with 1M entries immediately
23// Lazy: allocates one int at a time, zero extra memory

Chaining Iterators

csharp
1IEnumerable<int> Where(IEnumerable<int> source, Func<int, bool> predicate)
2{
3    foreach (var item in source)
4    {
5        if (predicate(item))
6            yield return item;
7    }
8}
9
10IEnumerable<int> Select(IEnumerable<int> source, Func<int, int> transform)
11{
12    foreach (var item in source)
13    {
14        yield return transform(item);
15    }
16}
17
18// Pipeline — nothing executes until foreach
19var result = Select(
20    Where(Range(1, 100), n => n % 2 == 0),
21    n => n * n
22);
23
24// Only now do the iterators execute, one element at a time
25foreach (var n in result.Take(5))
26{
27    Console.WriteLine(n);  // 4, 16, 36, 64, 100
28}

This is exactly how LINQ works internally — each LINQ method returns a lazy IEnumerable using yield return.

yield break

yield break ends the iterator immediately:

csharp
1IEnumerable<int> TakeWhilePositive(IEnumerable<int> source)
2{
3    foreach (var item in source)
4    {
5        if (item < 0)
6            yield break;  // Stop iteration entirely
7
8        yield return item;
9    }
10}
11
12var data = new[] { 3, 7, 2, -1, 5, 8 };
13foreach (var n in TakeWhilePositive(data))
14{
15    Console.WriteLine(n);  // 3, 7, 2
16}

Infinite Sequences

csharp
1IEnumerable<int> Fibonacci()
2{
3    int a = 0, b = 1;
4    while (true)
5    {
6        yield return a;
7        (a, b) = (b, a + b);
8    }
9}
10
11// Take only what you need — the infinite sequence is lazy
12foreach (var n in Fibonacci().Take(10))
13{
14    Console.Write($"{n} ");  // 0 1 1 2 3 5 8 13 21 34
15}

Without yield return, an infinite sequence would require an infinite loop that never returns. Lazy evaluation makes it practical.

Async Iterators (C# 8+)

csharp
1async IAsyncEnumerable<string> ReadLinesAsync(string path)
2{
3    using var reader = new StreamReader(path);
4    while (await reader.ReadLineAsync() is { } line)
5    {
6        yield return line;
7    }
8}
9
10await foreach (var line in ReadLinesAsync("data.txt"))
11{
12    Console.WriteLine(line);
13}

IAsyncEnumerable<T> combines yield return with await, enabling lazy asynchronous iteration.

Common Pitfalls

  • Deferred execution surprises: Code inside an iterator method does not run until the first MoveNext() call. If the method has parameter validation, the exception is not thrown until iteration begins, not when the method is called. Validate parameters in a non-iterator wrapper that calls the private iterator.
  • Multiple enumeration: Each foreach over an IEnumerable returned by yield return re-executes the entire method from the start. If the method reads from a database or file, it reads again. Call .ToList() to materialize the results when multiple iterations are needed.
  • Using yield return in try/catch: yield return cannot appear inside a try block that has a catch clause. It can appear in a try block with only a finally clause. This is a compiler limitation due to how the state machine is generated.
  • Forgetting that yield return keeps the method alive: Local variables in a yield return method are not garbage collected until iteration is complete or the enumerator is disposed. For methods that hold IDisposable resources, use try/finally with yield return to ensure cleanup.
  • Returning IEnumerable but calling .Count() or .ToList() immediately: If the caller always materializes the sequence with .ToList() or .Count(), the lazy evaluation provides no benefit. Use yield return when consumers typically iterate partially or chain with other LINQ operations.

Summary

  • yield return creates lazy iterators that generate elements one at a time
  • Only one element is in memory at a time — ideal for large or infinite sequences
  • The compiler transforms the method into a state machine that pauses and resumes
  • yield break terminates the iterator early
  • LINQ methods (Where, Select, Take) use yield return internally
  • Async iterators (IAsyncEnumerable<T>) combine yield return with await in C# 8+
  • Validate parameters in a non-iterator wrapper to get immediate exceptions

Course illustration
Course illustration

All Rights Reserved.