为什么List <T> .ForEach允许其列表被修改?
如果我使用:
var strings = new List<string> { "sample" }; foreach (string s in strings) { Console.WriteLine(s); strings.Add(s + "!"); }
在foreach
的Add
抛出一个InvalidOperationExceptionexception(集合被修改;枚举操作可能不会执行),我认为这是合乎逻辑的,因为我们正在从我们脚下扯下地毯。
但是,如果我使用:
var strings = new List<string> { "sample" }; strings.ForEach(s => { Console.WriteLine(s); strings.Add(s + "!"); });
它通过循环立即在脚中自我射击,直到抛出OutOfMemoryException。
这对我来说是一个惊喜,因为我一直认为List.ForEach不是为了foreach
就是为了包装。
有没有人有解释如何和为什么这种行为?
(由ForEach循环为无限重复的generics列表启发)
这是因为ForEach
方法不使用枚举器,它使用for
循环遍历项目:
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]); } }
(用JustDecompile获得的代码)
由于枚举器没有被使用,所以它从不检查列表是否已经改变,并且从未达到for
循环的结束条件,因为每次迭代都增加_size
。
List<T>.ForEach
是通过内部实现的,所以它不使用枚举器,它允许修改集合。
因为连接到List类的ForEach在内部使用直接连接到其内部成员的for循环,您可以通过下载.NET框架的源代码来看到这一点。
http://referencesource.microsoft.com/netframework.aspx
作为一个foreach循环首先是一个编译器优化,但也必须作为观察者对集合进行操作 – 所以如果集合被修改,它会抛出一个exception。
我们知道这个问题,原来是这样写的。 不幸的是,我们不能改变它,因为它现在会阻止以前工作的代码运行:
var list = new List<string>(); list.Add("Foo"); list.Add("Bar"); list.ForEach((item) => { if(item=="Foo") list.Remove(item); });
Eric Lippert指出,这种方法本身的用处是有问题的,所以我们没有将它包含在.NET for Metro风格的应用程序(即Windows 8应用程序)中。
David Kean(BCL团队)