Enumerating collections that change in C#
Posted: February 28, 2015 Filed under: .NET, C# | Tags: .NET, C# 3 CommentsIf 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
Sir but if I use this it work fine.
foreach (var payRunDetail in payrun.PayRunDetails.ToList())
{
payRunDetail.TimeSheetManager.TimeSheetStatus = (int)TimeSheetStatusType.ReadyForInterpretation;
Context.PayRunDetails.Remove(payRunDetail);
}
@Ankit Rana
You are modifying an object in the collection and not the collection itself. This is allowed.
I implemented this using a custom enumerator. Not sure where I got the idea. I have another one for dictionary collections. To call it:
For Each s As String In New CollectionReverseEnumerator(someNames)
someNames.Remove(s)
Next
Public NotInheritable Class CollectionReverseEnumerator
Implements IEnumerable
Implements IEnumerator
Private Property Position As Int32
Private Property Source As ICollection
Public Sub New(ByVal aoList As ICollection)
Source = aoList
Position = Source.Count
End Sub
Public ReadOnly Property Current() As Object Implements System.Collections.IEnumerator.Current
Get
Try
Return Source(Position)
Catch ex As IndexOutOfRangeException
Throw New InvalidOperationException
End Try
End Get
End Property
Public Function MoveNext() As Boolean Implements System.Collections.IEnumerator.MoveNext
Position -= 1
Return (Position > -1)
End Function
Public Sub Reset() Implements System.Collections.IEnumerator.Reset
Position = Source.Count
End Sub
Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
Return Me
End Function
End Class