Why I should not modify a collection when I am iterating on it?
Some collections can be modified when iterating, so it's not globally bad. In most cases it's very difficult to write an effective iterator that will work correctly even when the underlying collection is modified. In many cases the exception is the iterator writer punting and saying that they just don't want to deal with it.
In certain cases it's not clear what the iterator should do when the underlying collection is changed. Some cases are clear-cut, but for others different people will expect different behavior. Whenever you're in that situation it's a sign that there is a deeper problem (that you shouldn't mutating a sequence that you're iterating)
It is possible to create a collection that support to be modified when iterating on it, without to have any other problems ? (NOTE: that the first answer can answer this one too)
Sure.
Consider this iterator for a list:
public static IEnumerable<T> IterateWhileMutating<T>(this IList<T> list)
{
for (int i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
If you remove an item at or before the current index from the underlying list then an item will be skipped while iterating. If you add an item at or before the current index the an item will be duplicated. But if you add/remove items past the current index during iteration then there won't be a problem. We could try to be fancy and make an attempt to see if an item was removed/added from the list and adjust the index accordingly, but it couldn't always work, so we wouldn't be able to handle all cases. If we had something like an ObservableCollection
then we could be notified of additions/removals and their indexes and adjust the index accordingly, thus allowing the iterator to handle mutatings of the underlying collection (as long as it's not in another thread).
Since an iterator for an ObservableCollection
can know both when any items are added/removed, and where they are, it can adjust it's position accordingly. I am unsure if the built in iterator properly handles mutation, but here is one that will handle any mutation of the underlying collection:
public static IEnumerable<T> IterateWhileMutating<T>(
this ObservableCollection<T> list)
{
int i = 0;
NotifyCollectionChangedEventHandler handler = (_, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
if (args.NewStartingIndex <= i)
i++;
break;
case NotifyCollectionChangedAction.Move:
if (args.NewStartingIndex <= i)
i++;
if (args.OldStartingIndex <= i) //note *not* else if
i--;
break;
case NotifyCollectionChangedAction.Remove:
if (args.OldStartingIndex <= i)
i--;
break;
case NotifyCollectionChangedAction.Reset:
i = int.MaxValue;//end the sequence
break;
default:
//do nothing
break;
}
};
try
{
list.CollectionChanged += handler;
for (i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
finally
{
list.CollectionChanged -= handler;
}
}
If an item is removed from "earlier" in the sequence, we continue normally without skipping an item.
If an item is added "earlier" in the sequence we won't show it, but we also won't show some other item twice.
If an item is moved from before the current position to after it will be shown twice, but no other item will be skipped or repeated. If an item is moved from after the current position to before the current position it won't be shown, but that's all. If an item is moved from either later in the collection to another spot later, there is no problem, and the move will be seen in the result, if it is moved from an earlier spot to another earlier spot, everything is fine and the move won't be "seen" by the iterator.
Replacing an item isn't a problem; it will only be seen if it's "after" the current position though.
Resetting the collection results in the sequence ending gracefully at the current position.
Note that this iterator won't handle situations with multiple threads. If another thread mutates the collection while another is iterating, bad things could happen (items being skipped or repeated, or even exceptions, such as index out of bounds exceptions). What this does allow is mutations during iteration in which there is either only one thread, or in which only one thread is ever executing code that moves the iterator or mutates the collection.
When the C# compiler generate the Enumerator interface implementation takes into account this such of things?
The compiler doesn't generate the interface implementation; a person does.