← Back to Articles         
Enumeration and Iteration

Enumeration and Iteration

Published on 12 Oct 2026     8 min read C# Collections & Generics
Enumeration and Iteration

Enumeration and iteration are used to access elements in collections such as List<T>, Dictionary<TKey, TValue>, and HashSet<T>.

This group covers collection modification during iteration, return, yield return, and yield break.

1. Can we modify a collection while iterating using foreach?

What is it?

In C#, foreach is used to iterate through a collection.

We cannot normally add or remove elements from a List<T> while iterating over it using foreach. Doing so invalidates its enumerator.

However, modifying properties of existing reference-type objects is generally allowed.

Example 1: Incorrect approach

List<int> numbers = new() { 10, 20, 30, 40 };

foreach (int number in numbers)
{
    if (number == 20)
    {
        numbers.Remove(number);
    }
}

Output:

System.InvalidOperationException:
Collection was modified; enumeration operation
may not execute.

Explanation:

  1. foreach starts iterating through the list.

  2. When the value is 20, Remove() modifies the collection.

  3. The collection's internal version changes.

  4. On the next iteration, the enumerator detects the modification.

  5. An InvalidOperationException is thrown.

Example 2: Correct approach using RemoveAll()

List<int> numbers = new() { 10, 20, 30, 40 };

numbers.RemoveAll(n => n == 20);

foreach (int number in numbers)
{
    Console.WriteLine(number);
}

Output:

10
30
40

RemoveAll() removes matching elements without an active foreach enumerator.

Example 3: Modify object properties

class Employee
{
    public string Name { get; set; } = "";
    public int Salary { get; set; }
}

List<Employee> employees = new()
{
    new Employee { Name = "John", Salary = 30000 },
    new Employee { Name = "David", Salary = 40000 }
};

foreach (Employee employee in employees)
{
    employee.Salary += 5000;
}

Explanation:

  • The number of elements remains unchanged.

  • Only the Salary property of each object is updated.

  • This is permitted for mutable reference-type objects.

Key Points

  • foreach uses an enumerator internally.

  • Adding or removing elements from List<T> during enumeration throws an exception on a subsequent enumerator operation.

  • Use RemoveAll(), a reverse for loop, or create a separate collection.

  • Updating properties of existing objects is generally allowed.

  • Different collection implementations may have different enumeration rules.

 

2. What happens if a collection is modified during enumeration?

What is it?

An enumerator maintains the state required to traverse a collection.

For standard collections such as List<T>, structural modifications invalidate an active enumerator.

How does it work internally?

List<T> maintains an internal version number.

List<int>
    |
    |-- Elements: [10, 20, 30]
    |
    |-- Version: 1
    |
    v
GetEnumerator()
    |
    |-- Captures Version: 1
    |
    v
MoveNext()
    |
    |-- Version 1 == 1
    |-- Continue
    |
    v
list.Add(40)
    |
    |-- List Version: 2
    |
    v
MoveNext()
    |
    |-- Version 1 != 2
    |
    v
InvalidOperationException

Example Program

List<string> names = new()
{
    "John",
    "David",
    "Peter"
};

foreach (string name in names)
{
    Console.WriteLine(name);

    if (name == "David")
    {
        names.Add("Robert");
    }
}

Output:

John
David

System.InvalidOperationException:
Collection was modified; enumeration operation
may not execute.

How can we avoid this?

Approach 1: Use a reverse for loop

List<int> numbers = new() { 10, 20, 30, 40 };

for (int i = numbers.Count - 1; i >= 0; i--)
{
    if (numbers[i] == 20)
    {
        numbers.RemoveAt(i);
    }
}

Iterating backward avoids skipping elements when removing them.

Approach 2: Iterate over a snapshot

foreach (int number in numbers.ToList())
{
    if (number == 20)
    {
        numbers.Remove(number);
    }
}

ToList() creates a separate copy for iteration, so modifying the original list does not invalidate the copy's enumerator.

Key Points

  • List<T> detects modifications using an internal version number.

  • An exception typically occurs on the next MoveNext() call.

  • A reverse for loop is useful when removing elements.

  • ToList() provides a snapshot but requires additional memory.

  • Concurrent collections such as ConcurrentDictionary<TKey, TValue> support different enumeration semantics.

 

3. What is the difference between return and yield return?

What is return?

return immediately exits a method and optionally returns a value.

What is yield return?

yield return produces one element at a time and suspends the iterator method, allowing execution to resume when the next element is requested.

It is commonly used for lazy evaluation.

Comparison

Feature

return

yield return

Purpose

Return a result

Produce an element

Method execution

Ends the method

Suspends the iterator

Multiple values

Return a collection

Yield values individually

Execution

Normal method execution

Deferred until enumeration

Memory

Depends on returned object

Can avoid materializing a collection

Return type

Any valid return type

Iterator-compatible type

Example 1: Using return

static List<int> GetNumbers()
{
    List<int> numbers = new();

    for (int i = 1; i <= 3; i++)
    {
        numbers.Add(i);
    }

    return numbers;
}

foreach (int number in GetNumbers())
{
    Console.WriteLine(number);
}

Explanation:

  1. GetNumbers() executes.

  2. All three numbers are added to the list.

  3. The completed list is returned.

  4. foreach iterates through the list.

Example 2: Using yield return

static IEnumerable<int> GetNumbers()
{
    for (int i = 1; i <= 3; i++)
    {
        Console.WriteLine($"Generating {i}");

        yield return i;
    }
}

foreach (int number in GetNumbers())
{
    Console.WriteLine($"Received {number}");
}

Output:

Generating 1
Received 1
Generating 2
Received 2
Generating 3
Received 3

How does yield return work internally?

Call GetNumbers()
       |
       v
Return IEnumerable<int>
       |
       v
foreach starts enumeration
       |
       v
MoveNext()
       |
       v
Generate 1
       |
       v
yield return 1
       |
       v
Suspend execution
       |
       v
foreach receives 1
       |
       v
MoveNext()
       |
       v
Resume execution
       |
       v
Generate 2
       |
       v
yield return 2
       |
       v
Continue until complete

The C# compiler transforms an iterator method into a state machine that tracks its execution state.

Real-Time Scenario

Suppose an application processes thousands of records.

With return, you might create a complete list before processing begins.

With yield return, you can generate records one at a time.

static IEnumerable<int> GetEmployeeIds()
{
    for (int i = 1; i <= 10000; i++)
    {
        yield return i;
    }
}

This avoids creating a list of all 10,000 IDs.

For database queries, however, yield return does not automatically guarantee streaming; that depends on the database access implementation and whether results have already been materialized.

Key Points

  • return ends a method.

  • yield return suspends an iterator and produces one value.

  • yield return enables lazy evaluation.

  • The compiler generates a state machine.

  • Iterator methods commonly return IEnumerable<T> or IEnumerator<T>.

  • yield return is useful for large or generated sequences.

 

4. What is the difference between yield return and yield break?

What is yield return?

yield return produces an element and suspends the iterator until enumeration continues.

What is yield break?

yield break immediately terminates the iterator without producing another element.

Comparison

Feature

yield return

yield break

Purpose

Produce an element

Stop enumeration

Returns a value

Yes

No

Execution

Suspends

Terminates

Further iteration

Possible

Not possible

Typical use

Generate values

Stop when a condition is met

Example Program

static IEnumerable<int> GetNumbers()
{
    for (int i = 1; i <= 10; i++)
    {
        if (i > 5)
        {
            yield break;
        }

        yield return i;
    }
}

foreach (int number in GetNumbers())
{
    Console.WriteLine(number);
}

Output:

1
2
3
4
5

Explanation

  1. The loop begins at i = 1.

  2. yield return produces numbers from 1 to 5.

  3. When i = 6, the condition i > 5 becomes true.

  4. yield break terminates the iterator.

  5. The remaining numbers are not generated.

Execution Flow

GetNumbers()
     |
     v
i = 1
     |
     v
yield return 1
     |
     v
i = 2
     |
     v
yield return 2
     |
     v
...
     |
     v
i = 5
     |
     v
yield return 5
     |
     v
i = 6
     |
     v
Condition i > 5?
     |
    Yes
     |
     v
yield break
     |
     v
Enumeration Completed

Real-Time Scenario

Suppose an application generates invoice numbers but must stop when a maximum limit is reached.

static IEnumerable<int> GenerateInvoices(int max)
{
    for (int i = 1; i <= 100; i++)
    {
        if (i > max)
        {
            yield break;
        }

        yield return i;
    }
}

Calling GenerateInvoices(5) generates only five invoice numbers.

Key Points

  • yield return produces the next element.

  • yield break terminates enumeration.

  • Both are used inside iterator methods.

  • yield break is useful for conditional termination.

  • The compiler manages iterator state automatically.

 

Important Interview Revision

Question

Key Answer

Can we add/remove items inside foreach?

Not normally for List<T>; it invalidates the enumerator.

What happens when a list is modified?

InvalidOperationException on a subsequent enumeration operation.

How to safely remove items?

Use RemoveAll(), reverse for, or iterate over a copy.

How does foreach work?

Uses an enumerator and repeatedly calls MoveNext().

What is return?

Exits a method, optionally returning a value.

What is yield return?

Produces one element and suspends the iterator.

What is lazy evaluation?

Values are generated only when requested.

What is yield break?

Immediately terminates the iterator.

How is yield implemented?

The compiler generates an iterator state machine.

Most important interview concepts: Enumerator invalidation, InvalidOperationException, lazy evaluation, deferred execution, IEnumerable<T>, and the difference between yield return and yield break.

← Back to Articles