C#: Enumerating collections that change
Introduction
If you try you remove an item from an IEnumerable while enumerating it using a foreach loop in C# you will get an InvalidOperationException saying that "Collection was modified; enumeration operation may not execute".
As an example, consider the below simple code snippet where I create a List<string> of names and iterate it through and try to remove all names that starts with an 'A':
//create a list with some strings
List<string> someNames = new List<string>();
someNames.Add("Bill");
someNames.Add("Mike");
someNames.Add("Alice");
someNames.Add("Trevor");
someNames.Add("Scott");
foreach (string s in someNames) {
//try to remove all names that start with an 'A'
if(s.StartsWith("A"))
someNames.Remove(s); //!!! THIS CODE WON'T WORK AT RUNTIME!!!
}
The compiler won't complain but running this code will cause an InvalidOperationException to be thrown. If you run the code above using the debugger in Visual Studio you may note that the exception is not thrown on the line where the Remove method is called though.
IEnumerator
This is because it is the enumerator itself - an enumerator is any class that implements the IEnumerator interface - that throws the exception. Why does it do this? Because enumerators are only used to read from a collection, they can't modify it, and if you modify an item in a collection that is currently being enumerated from some other piece of code it may lead to unexpected and confusing results.
When implementing the IEnumerator interface yourself you should therefore remember to explicitly throw an InvalidOperationException in the MoveNext() method if the collection has changed during the enumeration.
Most of the built-in implementations of the IEnumerable interface in the .NET Framework behave this way. If you for example try to remove a DataRow of a DataTable while iterating over the Rows collection of the DataTable as per the below sample code you will also get an InvalidOperationException despite the fact that the data row is not actually removed from the DataTable until the AcceptChanges() method is called. It is only flagged for deletion (the RowState of the DataRow becomes Deleted) but still the enumerator throws the exception as the state of the collection is modified:
DataTable dt = new DataTable();
//load some data into the DataTable...
foreach (DataRow dr in dt.Rows) {
//some condition here...
dr.Delete(); //THIS CODE DOESN'T WORK AT RUNTIME!!!
}
For
What you should do in this cases like this is to simply replace the foreach loop with a for loop and iterate through the collection backwards, i.e. you start from the last index of the collection and then decrement the iterator variable (int i) by 1 in each iteration until you reach the first index (index = 0).
The following code is functionally equivalent to the failing code above but it doesn't throw any exception:
List<string> someNames = new List<string>();
someNames.Add("Bill");
someNames.Add("Mike");
someNames.Add("Alice");
someNames.Add("Trevor");
someNames.Add("Scott");
for (int i = someNames.Count - 1; i >= 0; i--) {
if (someNames[i].StartsWith("A"))
someNames.Remove(someNames[i]);
}
You could also iterate from the first to the last index as usual but then you must remember to decrement the iterator variable whenever you actually do remove an item in the loop. The following code also works as expected:
for (int i = 0; i < someNames.Count; i++) {
if (someNames[i].StartsWith("A")) {
someNames.Remove(someNames[i]);
i--; //decrease the value of the iterator variable
}
}
Bottom line is that you should never remove an item from or change the state of an IEnumerable that is currently being enumerated using a foreach loop. In these cases you should simply replace the foreach loop with a for loop.
While
The same rule also applies to while loops that are explicitly calling the MoveNext() method of an IEnumerator. The following code will for example also throw an InvalidOperationException for the same reason as the foreach loop above does:
using (IEnumerator<string> enumerator = someNames.GetEnumerator())
{
bool moveNext = enumerator.MoveNext();
while (moveNext)
{
if (enumerator.Current.StartsWith("A"))
someNames.Remove(enumerator.Current); //!!! THIS CODE WON'T WORK AT RUNTIME!!!
moveNext = enumerator.MoveNext();
}
}
Additional Resources
for (C# Reference): http://msdn.microsoft.com/en-us/library/ch45axte.aspx?f=255&MSPPError=-2147217396
foreach, in (C# Reference): http://msdn.microsoft.com/en-us/library/ttw7t8t6.aspx
IEnumerator Interface: http://msdn.microsoft.com/en-us/library/system.collections.ienumerator(v=vs.110).aspx
while (C# Reference): http://msdn.microsoft.com/en-us/library/2aeyhxcd.aspx