Intro to Yield in C# - What it is, how to use it, and when it is useful
When you first come across the yield keyword in C#, it can seem puzzling. How exactly does it work? When should you use yield return instead of a traditional return statement? To get a complete understanding, we’re going to walk through a detailed explanation based on Tim Corey’s excellent YouTube tutorial: "Intro to Yield in C# - What it is, how to use it, and when it is useful."
In this guide, we'll reference specific points in Tim’s video by timestamp for easier navigation and include practical examples to show how yield transforms the way you handle data streams, large collections, and lazy evaluation.
Introduction to the Yield Keyword in C
Tim starts by introducing the yield keyword and highlighting that it can often be confusing for developers encountering it for the first time. He explains that the yield statement allows a method to pause execution, maintain its state, and then continue from where it left off upon the next call. Tim emphasizes that understanding yield is critical for processing data efficiently, particularly when dealing with large datasets or implementing custom iteration logic.
Setting up a Simple Example: Class Program and Static Void Main
To eliminate distractions, Tim creates a simple Console Application in Visual Studio, called "YieldDemoApp."
What Yield Actually Does in C
Tim then dives deeper into theory. At 2:04, he describes the behavior of yield: Instead of processing an entire collection at once, the yield statement holds a spot — like putting a thumb into a book — so that execution can pause and resume later.
This behavior is crucial for deferred execution, where values are generated only when needed, rather than pre-computing everything upfront. Tim's description clearly sets the foundation for understanding how yield return works.
Writing Demo Code
In the static void Main method of the Class Program, he sets up basic bookend messages like "Start of the app" and "End of the app" using Console.WriteLine, helping to visualize the flow clearly when working with a foreach loop later.
This initial code example focuses purely on the yield implementation without involving any UI complexities.
Creating the PersonModel Class
To demonstrate, Tim creates a PersonModel class with FirstName and LastName properties and a constructor. When a PersonModel object is created, a message is printed, indicating which user was initialized. This helps visualize when objects are being created versus when they're being consumed.
This simple generate code step sets the stage for working with custom iterators.
Building the DataAccess Class with a Traditional List Return
At 5:06, Tim moves to a DataAccess class with a GetPeople method returning IEnumerable
This iterator method loads all the objects immediately into memory before iteration begins — an important point Tim later contrasts with using yield.
Demonstrating Memory Usage with List
After running the foreach loop, Tim shows that all three users are created before the first element is even read. This highlights a potential problem when dealing with large collections or large datasets — high memory usage.
Tim explains that if we had, say, a thousand users, we would have created a thousand objects even if we only needed a few, resulting in inefficient memory allocation.
Changing to Yield Return for Deferred Execution
At 10:01, Tim modifies GetPeople to use yield return instead of creating a temporary collection (a List). Each yield return statement directly emits one PersonModel at a time.
This critical code inside the method allows lazy evaluation, where the next element is generated only when needed by the foreach.
Tim also clarifies that the method's return type must be IEnumerable
Debugging: How Compiler Generates Code for Yield
Tim uses breakpoints to step through the process. He shows that before the first MoveNext call, the sequence is empty. Only when foreach needs the next value does the compiler trigger the iterator block and execute the yield return num line, which initializes and returns the PersonModel.
Tim highlights that the compiler generates special state machines under the hood to manage the paused and resumed execution.
Benefits and Efficiency of Using Yield Return
Tim explains why yield is so efficient:
You can fetch one record at a time.
You can limit the int count you need.
You avoid loading the entire collection into memory.
- You improve scalability, especially when working with large file processing or data streams.
By using LINQ's .Take(2), Tim demonstrates how only two objects are initialized even though three yield return statements exist — highlighting deferred execution in action.
Practical Example: Prime Number Generator Using Yield
Tim builds a new static IEnumerable
This example shows that yield is critical for handling infinite sequences safely.
Without yield, the code would crash due to unlimited memory consumption. Using yield, however, deferred execution ensures that numbers are generated on demand.
Fetching Prime Numbers with Take()
Tim then shows how to safely fetch only a fixed number of prime numbers:
var primeNumbers = Generators.GetPrimeNumbers().Take(10000);
var primeNumbers = Generators.GetPrimeNumbers().Take(10000);
This code written inside static void Main efficiently fetches just 10,000 primes.
Because yield return produces numbers one-by-one, memory usage remains low.
Custom Iteration: Using GetEnumerator and MoveNext
Tim takes things further by explaining how to control iteration manually.
He creates a var iterator = primeNumbers.GetEnumerator(), then uses a for loop with int i and calls iterator.MoveNext() to fetch elements manually.
This manual approach enables custom iteration — asking for next value only when needed, and showing the method resumes exactly from where it last paused.
A Common Mistake: Using ToList() on Yielded Collections
At 36:45, Tim warns: converting a yield sequence to a List with .ToList() causes immediate full evaluation.
If you call .ToList() on an infinite sequence, you risk crashing your app.
Tim stresses that yield return is intended for lazy evaluation, and that calling .ToList() breaks this pattern by forcing full memory materialization.
When working with LINQ methods, Tim recommends being cautious about where you introduce .ToList().
Summary: Why Yield is a Powerful Tool
Tim wraps up by reinforcing that while the yield keyword adds slight overhead (holding a state machine), the benefits of reduced memory usage and lazy evaluation make it a powerful tool when working with:
Large datasets
Large files
Custom iterators
Infinite sequences
Data streams
- Deferred processing
He suggests practicing with yield in different projects to develop a deeper understanding of yield return and avoid common pitfalls.
Tim concludes by inviting viewers to share how they have used yield in production code.
Conclusion
By walking through Tim Corey’s video, we’ve seen the immense benefits the yield keyword brings to C#. From creating custom iterators to managing large collections efficiently, yield return allows function returns to be smarter and more memory-efficient. Whether you're working with var number, var point, var reader, var connection, or large datasets, mastering yield can elevate your C# coding skills dramatically.
If you haven't explored using yield before, now is the perfect time to practice it through simple examples and better understand how yield return works inside the C# compiler. Do check out Tim's official YouTube Channel for more insightful videos.