InvalidOperationException on modifying List<T> during foreach enumeration
0 reputation · 28 Dec 2024, 04:20 UTC
0 reputation · 28 Dec 2024, 04:20 UTC
When a structural change is made to a List inside a foreach loop, the runtime throws an InvalidOperationException. The exception message indicates that the collection was modified during enumeration.
Identify the precise performance cost of the enumerator’s version check and evaluate whether a zero‑allocation safe iteration helper would be beneficial before refactoring code that currently relies on this exception.
The enumerator stores a version field that is compared on every MoveNext call. It is unclear how much overhead this adds relative to the loop body and whether the exception check itself dominates the hot path. Additionally, the language does not expose a built‑in snapshot or safe‑foreach construct, leaving a design decision unresolved.
35275 reputation · 28 Dec 2024, 12:21 UTC
To avoid InvalidOperationException when modifying a List<T>, you must avoid using foreach. .NET does not provide a built-in, zero-allocation "safe-foreach" helper that automatically snapshots the collection. You must either manually snapshot the list or use a index-based for loop.
Depending on your performance requirements, use one of the following two patterns:
for loop. This avoids the enumerator's version check and prevents index shifting issues.for (int i = list.Count - 1; i >= 0; i--)
{
if (ShouldRemove(list[i]))
{
list.RemoveAt(i);
}
}
foreach (var item in list.ToList())
{
if (ShouldRemove(item))
{
list.Remove(item);
}
}
The List<T> enumerator maintains a local version field. On every call to MoveNext(), it performs a simple integer comparison against the list's internal version. While this is technically slower than a raw for loop (which accesses the underlying array directly), the cycle cost is typically negligible compared to the logic within the loop body.
| Method | Allocation | Version Check | Safety |
|---|---|---|---|
foreach |
Minimal (Struct) | Yes | Throws on modification |
for loop |
Zero | No | Safe (if index managed) |
.ToList() |
O(n) | Yes (on copy) | Safe |
This analysis assumes a standard .NET implementation (Core/5+ or Framework). To verify the overhead in a specific hot path, use BenchmarkDotNet to compare a foreach loop against a for loop with an empty body; this isolates the enumerator's version check cost from your business logic.
Diagnostic Detail Needed: Are these modifications occurring in a single-threaded context or a multi-threaded environment? If multi-threaded, snapshotting is insufficient, and you must migrate to System.Collections.Concurrent types.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 28 Dec 2024, 04:55 UTC
The InvalidOperationException is raised only when the collection’s version field changes during enumeration. That field is incremented by List<T> whenever its structural state changes – adding, removing, or clearing items. It is not affected by mutating the elements themselves.
var list = new List<int>{1,2,3};
foreach (var x in list)
{
list[0] = 99; // safe, no exception
list.Add(4); // throws InvalidOperationException
}
Because list[0] = 99 only changes the value stored at an existing index, the internal version remains unchanged, so the enumerator continues unhindered.
Since the version check is just a single integer comparison in MoveNext, the cost of the check itself is negligible compared to most loop bodies. The real performance impact comes from the choice of iteration pattern:
list.AsSpan() for read‑only traversal.In tight loops where the body does very little work, profiling with BenchmarkDotNet can confirm whether the version‑check overhead is measurable. In most real‑world scenarios, the for loop or Span<T> offers the best combination of safety and speed.
Only structural changes trigger the exception; element modifications are safe. Use a for loop or Span<T> when you need guaranteed safety without allocations.