Why do you need yield in C#? The secret of lazy collections
Imagine: you are writing a method that should return a list of numbers from 1 to a million. The first thing that comes to mind is to create a List<int>, fill it, and return it. But what if the calling code only wants the first 10 elements? You still spent time creating a million objects, occupied memory — and all for nothing.
This is where the yield keyword in C# comes into play. It is not just syntactic sugar, but a powerful tool for creating lazy sequences. In this article, we will break down yield return and yield break with real examples, understand how they work under the hood, and learn to apply them in everyday development.
This tutorial is based on a real Stack Overflow question that gathered thousands of views. We have adapted it for educational content, adding step-by-step C# code examples and useful practical tips.
What is yield in C#? In simple words
yield is a contextual keyword in C# that tells the compiler: "This method is an iterator." When you write yield return, the method does not execute everything at once. Instead, it returns elements one by one, as the calling code requests them. This is called lazy execution.
Unlike a regular return, which terminates the method, yield return pauses the method, saves its state, and returns the current element. On the next call, the method continues execution from where it left off.
Syntax: yield return and yield break
In C#, there are two forms of yield:
- yield return — returns the next element of the sequence.
- yield break — immediately terminates the iteration (like
breakin a loop).
A method using yield must return one of these types:
IEnumerable<T>IEnumerableIAsyncEnumerable<T>(starting from C# 8.0)IEnumerator<T>orIEnumerator
First example: from simple to complex
Let's write a simple iterator that generates numbers from 1 to 5:
public static IEnumerable<int> GetNumbers(){ yield return 1; yield return 2; yield return 3; yield return 4; yield return 5;}
// Usage:foreach (int number in GetNumbers()){ Console.WriteLine(number);}Output:
12345At first glance, this looks like a regular list. But the difference is huge: each element is generated only when requested by foreach. No memory allocation for the entire list!
How does it work under the hood?
The C# compiler turns the method with yield into a full-fledged state machine. It creates a class that implements IEnumerable<T> and IEnumerator<T>. This class stores:
- The current state (which line of the method we stopped at).
- The method's local variables.
- A reference to the current object (
this), if the method is non-static.
Each time MoveNext() is called (which foreach does), the state machine resumes execution from the last stopping point.
Real example: reading a large file
Imagine you need to process a 10 GB log file. If you read it entirely into memory, the program will crash with an OutOfMemoryException. With yield, you can read the file line by line and process each line on the fly:
public static IEnumerable<s