Why does List<T>.ForEach allow its list to be modified?
C#ListForeachC# Problem Overview
If I use:
var strings = new List<string> { "sample" };
foreach (string s in strings)
{
Console.WriteLine(s);
strings.Add(s + "!");
}
the Add
in the foreach
throws an InvalidOperationException (Collection was modified; enumeration operation may not execute), which I consider logical, since we are pulling the rug from under our feet.
However, if I use:
var strings = new List<string> { "sample" };
strings.ForEach(s =>
{
Console.WriteLine(s);
strings.Add(s + "!");
});
it promptly shoots itself in the foot by looping until it throws an OutOfMemoryException.
This comes as a suprise to me, as I always thought that Listforeach
or for for
.
Does anyone have an explanation for the how and the why of this behavior?
(Inpired by https://stackoverflow.com/questions/9311272/foreach-loop-for-a-generic-list-repeated-endlessly)
C# Solutions
Solution 1 - C#
It's because the ForEach
method doesn't use the enumerator, it loops through the items with a for
loop:
public void ForEach(Action<T> action)
{
if (action == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
for (int i = 0; i < this._size; i++)
{
action(this._items[i]);
}
}
(code obtained with JustDecompile)
Since the enumerator is not used, it never checks if the list has changed, and the end condition of the for
loop is never reached because _size
is increased at every iteration.
Solution 2 - C#
List<T>.ForEach
is implemented through for
inside, so it does not use enumerator and it allows to modify the collection.
Solution 3 - C#
Because the ForEach attached to the List class internally uses a for loop that is directly attached to its internal members -- which you can see by downloading the source code for the .NET framework.
http://referencesource.microsoft.com/netframework.aspx
Where as a foreach loop is first and foremost a compiler optimization but also must operate against the collection as an observer -- so if the collection is modified it throws an exception.
Solution 4 - C#
We know about this issue, it was an oversight when it was originally written. Unfortunately, we can't change it because it would now prevent this previously working code from running:
var list = new List<string>();
list.Add("Foo");
list.Add("Bar");
list.ForEach((item) =>
{
if(item=="Foo")
list.Remove(item);
});
The usefulness of this method itself is questionable as Eric Lippert pointed out, so we didn't include it for .NET for Metro style apps (ie Windows 8 apps).
David Kean (BCL Team)