Introduction
The yield keyword in C# allows you to create iterators that return values one at a time, instead of generating a full collection upfront. This is classic example of lazy loading, meaning elements are only processed when needed, leading to better performance and reduced memory usage. It's especially useful for handling large data sets.
The two primary uses of yield are
- yield return: Which returns a value from an iterator and pauses execution.
- yield break: Ends the iteration immediately.
How Yield Works?
Let’s consider a basic example demonstrating how yield returns elements lazily.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (int num in GetNumbers())
{
Console.WriteLine(num);
}
}
static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
}
//Output
1
2
3
Code Snippet 1
How does it Work?
- The foreach loop calls GetNumbers().
- The method runs until it encounters the first yield return, then pauses and gives control back to the loop.
- Each time the loop requests the next item, execution resumes right where it left off.
- This continues until there are no more yield return statements.
Example 1. Real-World Example: Processing Large Files
A common use case for yield return is when dealing with large files where loading all data into memory at once would be inefficient.
Benefits
- Reads-only: one line at a time instead of loading the whole file.
- Memory-Saving: Prevents high memory consumption for large files.
- Stateful: If interrupted, it can resume from where it left off instead of restarting.
using System;
using System.Collections.Generic;
using System.IO;
class Program
{
static void Main()
{
foreach (string line in ReadLargeFile("bigfile.txt"))
{
Console.WriteLine(line); // Processes one line at a time
}
}
static IEnumerable<string> ReadLargeFile(string filePath)
{
using (StreamReader reader = new(filePath))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
yield return line; // Returns one line at a time
}
}
}
}
Code Snippet 2
Yield vs. Normal Return
If we remove yield return, we would have to store all results in a List and return it, leading to higher memory usage.
Feature |
Using yield return |
Using List<T> |
Execution |
Returns one item at a time |
Processes everything at once |
Memory Usage |
Efficient (lazy loading) |
Stores everything in memory |
Performance |
Faster for large datasets |
Slower for large datasets |
Complexity |
Simple to implement |
Requires manual list creation |
Example 2. Filtering using Yield
Let’s now look at how yield return can simplify filtering operations.
- The method does not create a full list of filtered items.
- Instead, it yields each matching Product one by one as they are found.
- If the calling code stops iterating early, unused elements aren't processed, saving resources.
public IEnumerable<Product> FilterBySize(IEnumerable<Product> products, Size size)
{
foreach (var p in products)
{
if (p.Size == size)
yield return p;
}
}
Code Snippet 3
Filtering without Yield
public IEnumerable<Product> FilterBySize(IEnumerable<Product> products, Size size)
{
List<Product> result = new List<Product>();
foreach (var p in products)
{
if (p.Size == size)
result.Add(p);
}
return result;
}
Code Snippet 4
Drawbacks
- Creates a new list, increasing memory usage.
- Stores all matching elements before returning, making it less efficient for large datasets.
Conclusion
The yield keyword in C# simplifies iterator creation, improves performance, and reduces memory usage. It is especially useful for:
- Generating sequences lazily (e.g., Fibonacci numbers, infinite sequences).
- Processing large datasets efficiently (e.g., reading files line by line).
- Filtering collections dynamically (e.g., FilterBySize method).
By leveraging yield return, you can write simpler, more efficient, and scalable C# code!
Cheers,
See you around.