C# is very strict about modify-while-iterating

Today I stumbled upon some behavior in C#’s List<> that I found very surprising. I had forgotten about RemoveAll() and basically implemented it myself:

var target = 0;
foreach (var each in list)
{
if (!predicate(each))
list[target++] = each;
}
list.RemoveRange(target, list.Count - target);

Apparently, this is not allowed. You cannot assign to any element in the List<> while you are iterating/enumerating it: The List<> implementation holds a ‘version’ number that is incremented any time a change is made, including assignments. When the Enumerator is advanced via MoveNext it checks for this version and throws the dreaded ‘Collection was modified’ exception.

Except that there shouldn’t really be a problem here, and the modification checking code is basically being too coarse. This code ‘compacts’ the list, copying elements where the predicate evaluates to true to the front of the list, and then cutting off the rest of the elements. There’s never really any doubt what each references. In fact, in other languages, this approach is even considered idiomatic to remove elements while iterating. In ancient C++, this is known as the remove/erase idiom, see also std::erase.

So why did the library designers of C# consider setting a value while iterating a problem? I don’t know, but at least now I have a story to remind of of RemoveAll()‘s existence.

2 thoughts on “C# is very strict about modify-while-iterating”

  1. I can give you a reasonable explanation for this strict rule: Consistency across different collections.

    While it is fine to assign to a list, as it causes no structural modification. This might not hold for dictionaries/maps, which are by the way not considered to be a collection in Java. However, in C# they are treated as collections.

    I am not a C# developer, but as far as I know you can use the syntax myMap[key]=value for both updating an entry (assigment) and inserting a new entry in the map (structural modifcation).

    I hope this helps!

    Kind regards

    Sören

    1. That’s a fair point, thank you! Maybe that was the reason, but they did not properly model it in the type system.

      They are both ICollections, but ICollection does not have indexing. That is in IList and IDictionary respectively, which are both sub-interfaces of ICollection.

      Also, it is very common to have actual implementations with less strict interfaces (weaker pre-conditions) than their interfaces. So I’m not convinced modeling it like this was a sound decision.

      Anecdotally: C++ has had the same syntax for std::vector and std::map assignments since before C# existed, and it has the same difference where the former is not a structural change, while the latter can be.

Leave a reply to mariuselvert Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.