Keywords: C# | Dictionary Modification | foreach Loop | Version Number Mechanism | Exception Handling
Abstract: This article delves into the root cause of the 'Collection was modified; enumeration operation may not execute' exception when modifying dictionary values during foreach iteration in C#. By analyzing the internal version number mechanism of dictionaries, it explains why value modifications disrupt iterators. Two primary solutions are provided: pre-copying key collections and creating modification lists for deferred application, supplemented by the LINQ ToList() method. Each approach includes detailed code examples and scenario analyses to help developers avoid common pitfalls and optimize data processing workflows.
Problem Background and Exception Analysis
In C# programming, dictionaries are commonly used key-value pair collections, but modifying their contents during foreach iteration can cause runtime exceptions. A typical scenario is data cleaning: suppose a dictionary represents pie chart data, and items with less than 5% share need to be merged into an "Other" category. Directly iterating over dictionary keys and modifying values triggers a System.InvalidOperationException with the message "Collection was modified; enumeration operation may not execute."
Root Cause of the Exception
Dictionaries internally maintain a version number to track modification states. When a value is set via the indexer (e.g., colStates[key] = 0), even if the key exists, the dictionary's version number increments. The foreach loop relies on an iterator, which captures the current version number upon initialization. If the version number changes during iteration, the iterator detects inconsistency and throws an exception to ensure traversal integrity.
This design, while strict, is justified: it prevents unpredictable behavior due to accidental collection state changes during iteration. For instance, allowing values to change mid-iteration could lead to logical errors or performance issues if the iterator is based on key or value collections. To simplify implementation, the .NET framework uses a single version number for the entire dictionary, rather than maintaining separate ones for keys, values, and entries.
Solution 1: Pre-copying the Key Collection
The most straightforward approach is to copy the key collection into a separate list before iteration, avoiding modifications to the dictionary while iterating over the original collection. Example code:
List<string> keys = new List<string>(colStates.Keys);
foreach(string key in keys)
{
double percent = colStates[key] / TotalCount;
if (percent < 0.05)
{
OtherCount += colStates[key];
colStates[key] = 0;
}
}
This method is simple and efficient, suitable for scenarios with moderate dictionary size and few modifications. It creates a copy of keys via new List<string>(colStates.Keys), ensuring the dictionary's version number remains unchanged during iteration. However, note that copying may incur memory overhead for large dictionaries.
Solution 2: Creating a Modification List for Deferred Application
Another method involves recording keys that need modification and applying changes uniformly after iteration. Example code:
List<string> keysToNuke = new List<string>();
foreach(string key in colStates.Keys)
{
double percent = colStates[key] / TotalCount;
if (percent < 0.05)
{
OtherCount += colStates[key];
keysToNuke.Add(key);
}
}
foreach (string key in keysToNuke)
{
colStates[key] = 0;
}
This approach avoids copying the entire key collection, making it suitable for large dictionaries or complex modification operations. It uses two separate loops: the first collects keys to process, and the second applies modifications. This enhances code clarity but may increase the number of iterations.
Supplementary Solution: Using the LINQ ToList() Method
For .NET Framework 3.5 and above, LINQ extension methods can simplify the code. Example code:
using System.Linq;
foreach(string key in colStates.Keys.ToList())
{
double percent = colStates[key] / TotalCount;
if (percent < 0.05)
{
OtherCount += colStates[key];
colStates[key] = 0;
}
}
The ToList() method internally creates a copy of the key collection, similar in principle to Solution 1, but with more concise code. It relies on LINQ and is suitable for modern C# development environments. However, in performance-critical scenarios, note that LINQ may introduce slight overhead.
Practical Recommendations and Conclusion
In practice, the choice of solution depends on specific needs: if the dictionary is small and code readability is prioritized, ToList() is recommended; if fine-grained memory control or handling large datasets is required, pre-copying keys or deferred modifications are more appropriate. Regardless of the method, the core principle is to avoid direct dictionary modifications during iteration to ensure code robustness.
Understanding the internal mechanisms of dictionaries helps prevent similar issues. For example, in concurrent scenarios, consider using thread-safe collections or synchronization mechanisms. Through the analysis in this article, developers can handle dictionary data more safely, enhancing application reliability.