Skip to main content

28. Iterators and yield return in C#

Level: Intermediate
Goal: Learn lazy evaluation with yield 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.

ℹ️ What You'll Learn
  • How yield return works (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

ConceptMeaningReturnsMemoryUse When
yield returnPause, return one value, resumeOne value at a timeOnly current + next bufferedLarge sequences, filters
Regular returnReturn complete collectionAll values at onceAll values in memorySmall lists, need count
IEnumerableLazy sequence interfaceEnumerable (can iterate)Deferred (on-demand)Chains, filters, pagination
ListEager concrete collectionAll itemsAll in memory immediatelyNeed indexing, count
ToList()Materialize lazy sequenceConcrete ListAll in memoryEnd of LINQ chain
Take(n)Get first N from lazyIEnumerable of N itemsOnly N itemsPagination, sampling
Skip(n)Skip first N from lazyIEnumerable restDeferred restPagination, 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

💻 Try It — Console App
💡 Paste into Program.cs and press F5⌥ GitHub
// Generate roll numbers - no large list in memory
foreach (var roll in GenerateRollNumbers("NCA", 2024, 40))
Console.WriteLine(roll);

// Paginate students - process page by page
var pages = Paginate(students, pageSize: 10);
foreach (var page in pages)
{
Console.WriteLine($"\n--- Page ---");
foreach (var s in page)
Console.WriteLine($" {s.Name}");
}

// Grade report - lazy
foreach (var (s, grade, result) in GetGradeReport(students))
Console.WriteLine($"{s.Name,-20} {grade,-4} {result}");

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

ℹ️ Video Tutorial

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

  1. Use yield return for large sequences - Memory efficient, lazy
  2. Use List when need indexing - yield doesn't support [index]
  3. Call .ToList() at end of chain - Materialize only when needed
  4. Return IEnumerable<T> from methods - Not List<T>, more flexible
  5. Don't materialize until endpoint - Where, Select, Skip, Take are lazy
  6. Cache if iterating multiple times - Avoid re-evaluating expensive chain
  7. Understand deferred execution - Chain builds query, .ToList() executes it
  8. Use LINQ lazy operators - Where, Select, Skip, Take, OrderBy on IEnumerable
  9. Profile before optimizing - Lazy isn't always faster (overhead of yield)
  10. Use yield break to exit early - Stops iteration gracefully

🎯 Q1: How does yield return work?

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.

🎯 Q2: What's the difference between List and IEnumerable with yield?

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.

🎯 Q3: What is deferred execution and why does it matter?

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.

🎯 Q4: When should I use yield return?

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;
}
🎯 Q5: Why is ToList() at the end important?

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.

🎯 Q6: How do I iterate same IEnumerable multiple times?

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 AI to Learn Faster

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.

Next Article

Nullable Types ->

nexcoding.in