28. Iterators and yield return in C#
Level: Intermediate
Goal: Learn lazy evaluation withyield return- process large data without loading everything into memory.
Iterators help you create lists one item at a time instead of building the whole list upfront.
Why? Imagine a file with 1 million student records. Loading all 1 million at once uses lots of memory. Iterators let you process one record at a time.
- How
yield returnworks (lazy evaluation) - Memory efficiency (yield vs ToList)
- IEnumerable vs IEnumerator
- Creating custom iterators
- Pagination, filtering with yield
- When to use yield (and when not to)
- Performance implications
yield return creates lazy sequence - values generated one at a time on demand, not all loaded into memory.
Quick Definitions
- Iterator - Method returning IEnumerable with yield return
yield return- Pause execution, return one value, resume on next call- Lazy evaluation - Generate values only when requested
- Deferred execution - Code runs later, not immediately
- IEnumerable - Interface for lazy sequences
- IEnumerator - Cursor moving through sequence one at a time
- Materialization - Convert lazy IEnumerable to concrete List with ToList()
- Pagination - Return data in chunks (first 10, next 10, etc.)
- Memory efficient - Process large data without loading all at once
- Streaming - Process data as it arrives, not batch
Iterators Reference
| Concept | Meaning | Returns | Memory | Use When |
|---|---|---|---|---|
yield return | Pause, return one value, resume | One value at a time | Only current + next buffered | Large sequences, filters |
| Regular return | Return complete collection | All values at once | All values in memory | Small lists, need count |
| IEnumerable | Lazy sequence interface | Enumerable (can iterate) | Deferred (on-demand) | Chains, filters, pagination |
| List | Eager concrete collection | All items | All in memory immediately | Need indexing, count |
| ToList() | Materialize lazy sequence | Concrete List | All in memory | End of LINQ chain |
| Take(n) | Get first N from lazy | IEnumerable of N items | Only N items | Pagination, sampling |
| Skip(n) | Skip first N from lazy | IEnumerable rest | Deferred rest | Pagination, skip header |
yield return vs Regular Return
// Eager - loads ALL 10,000 students into memory first
static List<Student> GetAllStudents()
{
var list = new List<Student>();
for (int i = 1; i <= 10000; i++)
list.Add(CreateStudent(i));
return list; // 10,000 objects in memory
}
// Lazy - generates one at a time, caller controls how many
static IEnumerable<Student> GetStudentsLazy()
{
for (int i = 1; i <= 10000; i++)
{
yield return CreateStudent(i); // pause, give one, wait for next
}
}
// If caller only needs first 10 - lazy only creates 10
var first10 = GetStudentsLazy().Take(10).ToList(); // 10 objects, not 10,000
School Management - Iterators
// Generate roll numbers
static IEnumerable<string> GenerateRollNumbers(string prefix, int startYear, int count)
{
for (int i = 1; i <= count; i++)
yield return $"{prefix}-{startYear}-{i:D4}";
// NCA-2024-0001, NCA-2024-0002, ...
}
// Filter with yield - custom lazy filter
static IEnumerable<Student> GetEligibleForExam(IEnumerable<Student> students)
{
foreach (var s in students)
{
if (s.AttendancePercentage < 75) continue;
if (s.OutstandingFees > 0) continue;
if (!s.HasPassed()) continue;
yield return s; // only eligible students yielded
}
}
// Paginate large result
static IEnumerable<IEnumerable<T>> Paginate<T>(IEnumerable<T> source, int pageSize)
{
var page = new List<T>();
foreach (var item in source)
{
page.Add(item);
if (page.Count == pageSize)
{
yield return page; // yield one page at a time
page = new List<T>();
}
}
if (page.Count > 0) yield return page;
}
// Grade report - lazy transform
static IEnumerable<(Student student, string grade, string result)> GetGradeReport(
IEnumerable<Student> students)
{
foreach (var s in students)
{
string grade = s.Percentage >= 90 ? "A+" : s.Percentage >= 35 ? "Pass" : "F";
string result = s.Percentage >= 35 ? "Pass" : "Fail";
yield return (s, grade, result);
}
}
Usage
When You'll Use This in SMS
SMS processes large data with iterators:
// 100,000 students - iterate one at a time, not load all
GetAllStudents() // 100k objects in memory
GetStudentsLazy() // Only current object in memory
// Pagination - show page 1 (10 students), not all 100k
PaginateStudents(students, pageSize: 10)
// Report generation - build line by line
GenerateReportLines(students) // Each line on demand
// Large file import - read one student per iteration
ImportStudentsFromFile("students.csv") // One at a time
Memory difference:
- Eager: All 100,000 students in memory = 500MB
- Lazy: One student at a time = 5KB
Iterators explained: yield return, lazy evaluation, IEnumerable, memory efficiency, pagination. Video coming soon. Subscribe to NexCoding YouTube for updates.
Common Mistakes
Wrong Using List for large sequences (loads all into memory):
// Loads ALL 1M students into memory
List<Student> all = GetAllStudentsFromDb(); // 1M objects!
var first10 = all.Take(10).ToList(); // Only needed 10
OK Use yield return for lazy evaluation:
IEnumerable<Student> all = GetAllStudentsLazy(); // Nothing loaded yet
var first10 = all.Take(10).ToList(); // Only loads 10
Wrong Calling ToList() too early (defeats lazy evaluation):
IEnumerable<Student> passed = GetEligibleStudents().ToList(); // Materialized!
var top10 = passed.Take(10); // Still materialized, defeats purpose
OK Call ToList() at end only:
IEnumerable<Student> passed = GetEligibleStudents(); // Lazy
var top10 = passed.Take(10).ToList(); // Materialize only at end
Wrong Modifying collection during iteration:
var students = GetStudents(); // Lazy
foreach (var s in students)
{
if (s.Percentage < 35)
students.Remove(s); // ERROR! Can't modify during iteration
}
OK Use Where to filter (doesn't modify source):
var students = GetStudents();
var passing = students.Where(s => s.Percentage >= 35);
Wrong Iterating same IEnumerable multiple times (evaluates each time):
IEnumerable<Student> passed = students.Where(s => s.Percentage >= 35);
var count = passed.Count(); // Evaluates chain
var first = passed.First(); // Evaluates chain AGAIN
OK Cache if iterating multiple times:
List<Student> passed = students.Where(s => s.Percentage >= 35).ToList();
var count = passed.Count; // Cached
var first = passed.First(); // Cached
Best Practices
- Use
yield returnfor large sequences - Memory efficient, lazy - Use List when need indexing - yield doesn't support [index]
- Call
.ToList()at end of chain - Materialize only when needed - Return
IEnumerable<T>from methods - NotList<T>, more flexible - Don't materialize until endpoint - Where, Select, Skip, Take are lazy
- Cache if iterating multiple times - Avoid re-evaluating expensive chain
- Understand deferred execution - Chain builds query,
.ToList()executes it - Use LINQ lazy operators - Where, Select, Skip, Take, OrderBy on IEnumerable
- Profile before optimizing - Lazy isn't always faster (overhead of yield)
- Use
yield breakto exit early - Stops iteration gracefully
yield return pauses method, returns one value, resumes from pause on next iteration.
IEnumerable<int> CountToThree()
{
Console.WriteLine("Start");
yield return 1; // Pause, return 1
Console.WriteLine("Between 1 and 2");
yield return 2; // Pause, return 2
Console.WriteLine("Between 2 and 3");
yield return 3; // Pause, return 3
Console.WriteLine("End");
}
foreach (var n in CountToThree())
Console.WriteLine($"Got {n}");
// Output:
// Start
// Got 1
// Between 1 and 2
// Got 2
// Between 2 and 3
// Got 3
// End
Method execution pauses at each yield, resumes on next iteration.
List: Eager. Creates ALL objects immediately, loads into memory. IEnumerable with yield: Lazy. Creates objects on-demand, one at a time.
// Eager - loads all 10,000
List<Student> list = new();
for (int i = 0; i < 10000; i++)
list.Add(CreateStudent(i)); // 10,000 in memory NOW
// Lazy - nothing loaded yet
IEnumerable<Student> lazy = GetStudentsLazy(); // No objects created
var first10 = lazy.Take(10).ToList(); // Only 10 created
Large dataset: yield saves memory. Need indexing: must use List.
Deferred execution = query built but not run until you materialize (call .ToList() or iterate).
IEnumerable<Student> query = students
.Where(s => s.Percentage >= 35)
.OrderBy(s => s.Name); // Nothing executed yet!
// Execution happens here
var result = query.ToList(); // Now Where and OrderBy run
Why? Chains can be built, modified, reused. Execution only when needed.
Data can change between building and executing. If students list changes, next iteration uses new data.
Use yield when: returning potentially large sequence, filtering/transforming as you go, streaming data.
DON'T use yield when: need to return count (Count() triggers full evaluation), need random access [index], sequence small enough to fit in memory.
// GOOD - yields one at a time
IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0) yield return i;
}
// DON'T - yield unnecessary for small list
IEnumerable<int> GetTop3()
{
yield return 1; // Just return List!
yield return 2;
yield return 3;
}
If you call .ToList() early, defeats lazy evaluation. All remaining LINQ operations run on List (eager), not lazy.
// BAD - materializes early
var result = students
.Where(s => s.Percentage >= 35)
.ToList() // Materialized here
.OrderBy(s => s.Name) // OrderBy runs on List, eager
.Take(10) // Already ordered all!
.ToList();
// GOOD - materializes at end
var result = students
.Where(s => s.Percentage >= 35) // Lazy
.OrderBy(s => s.Name) // Lazy
.Take(10) // Lazy
.ToList(); // Materialize once at end
Call .ToList() only at very end when you need concrete List.
Calling same IEnumerable multiple times re-evaluates chain. If expensive, cache to List.
IEnumerable<Student> passed = students.Where(s => s.Percentage >= 35);
var count = passed.Count(); // Evaluates WHERE once
var list = passed.ToList(); // Evaluates WHERE AGAIN!
var first = passed.First(); // Evaluates WHERE AGAIN!
// Better - cache once
List<Student> passed = students.Where(s => s.Percentage >= 35).ToList();
var count = passed.Count; // Cached, no re-eval
var first = passed.First(); // Cached
Multiple iterations = cache. Single iteration = keep lazy.
Use ChatGPT, Claude, or Copilot to go deeper on C# yield return and iterators. Try these prompts:
"Explain yield return in C# - what makes it different from returning a List?""What is lazy evaluation and why does it matter for memory usage?""When should I use IEnumerable with yield vs returning a List in C#?""Show me a real-world example where yield return saves memory"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.