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:
-
foreachstarts iterating through the list. -
When the value is
20,Remove()modifies the collection. -
The collection's internal version changes.
-
On the next iteration, the enumerator detects the modification.
-
An
InvalidOperationExceptionis 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
Salaryproperty of each object is updated. -
This is permitted for mutable reference-type objects.
Key Points
-
foreachuses an enumerator internally. -
Adding or removing elements from
List<T>during enumeration throws an exception on a subsequent enumerator operation. -
Use
RemoveAll(), a reverseforloop, 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
forloop 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 |
|
|
|---|---|---|
|
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:
-
GetNumbers()executes. -
All three numbers are added to the list.
-
The completed list is returned.
-
foreachiterates 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
-
returnends a method. -
yield returnsuspends an iterator and produces one value. -
yield returnenables lazy evaluation. -
The compiler generates a state machine.
-
Iterator methods commonly return
IEnumerable<T>orIEnumerator<T>. -
yield returnis 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 |
|
|
|---|---|---|
|
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
-
The loop begins at
i = 1. -
yield returnproduces numbers from 1 to 5. -
When
i = 6, the conditioni > 5becomes true. -
yield breakterminates the iterator. -
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 returnproduces the next element. -
yield breakterminates enumeration. -
Both are used inside iterator methods.
-
yield breakis useful for conditional termination. -
The compiler manages iterator state automatically.
Important Interview Revision
|
Question |
Key Answer |
|---|---|
|
Can we add/remove items inside |
Not normally for |
|
What happens when a list is modified? |
|
|
How to safely remove items? |
Use |
|
How does |
Uses an enumerator and repeatedly calls |
|
What is |
Exits a method, optionally returning a value. |
|
What is |
Produces one element and suspends the iterator. |
|
What is lazy evaluation? |
Values are generated only when requested. |
|
What is |
Immediately terminates the iterator. |
|
How is |
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.