我不明白为什么List<T>.ForEach()
扩展方法在后台实现for
循环。这打开了修改集合的可能性。在这种情况下,正常的foreach
会抛出异常,那么ForEach()
肯定也应该以同样的方式做出反应吗?
如果出于任何原因必须对集合进行变异,那么您肯定应该在for
循环中手动迭代该集合吗?
这里foreach
和List<T>.ForEach()
之间似乎有点语义矛盾。
我是不是错过了什么?
只有BCL团队的一名成员可以肯定地告诉我们,但List<T>.ForEach
允许您修改列表可能只是疏忽。
首先,David B的答案对我来说没有意义。是List<T>
,而不是C#,它检查你是否在foreach
循环中修改了列表,如果你修改了,就会抛出InvalidOperationException
。这与你使用的语言无关。
其次,文档中有这样的警告:
修改Action主体中的基础集合<T>委托不受支持,并导致未定义的行为。
我发现BCL团队不太可能希望像ForEach
这样的简单方法具有未定义的行为。
第三,从.NET 4.5开始,如果委托修改列表,List<T>.ForEach
将抛出一个InvalidOperationException
。如果一个程序依赖于旧的行为,当它被重新编译到目标.NET 4.5时,它将停止工作。微软愿意接受这一突破性的改变,这一事实有力地表明,最初的行为是无意的,不应该被依赖。
作为参考,以下是List<T>.ForEach
在.NET 4.0中的实现方式,直接来自参考来源:
public void ForEach(Action<T> action) {
if( action == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.EndContractBlock();
for(int i = 0 ; i < _size; i++) {
action(_items[i]);
}
}
以下是它在.NET4.5:中的变化
public void ForEach(Action<T> action) {
if( action == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.EndContractBlock();
int version = _version;
for(int i = 0 ; i < _size; i++) {
if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5) {
break;
}
action(_items[i]);
}
if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
foreach
是一个C#语言元素。它遵循C#的规则。
List<T>.ForEach
是一个.NET Framework方法。它遵循.NET的规则,而foreach
并不存在。
这是"语言与框架"混淆的一个例子。框架方法必须在许多语言中工作,并且这些语言(通常)具有矛盾的语义。
这种"语言与框架"混淆的另一个例子是.net 3和.net 3.5之间对Enumerable.Cast
的突破性更改。在.NET3中,Cast
使用了C#语义。在.net 3.5中,它被更改为使用.net语义。
因为List.ForEach遵循MSDN中的定义:
对列表的每个元素执行指定的操作。
这意味着在元素上执行的Action
可能会更改元素或集合本身。在这种情况下,没有其他方法(如果不创建成本高昂的克隆集合,如果是可能的)可以提供此功能,然后使用简单的for
。
如果在foreach
中的迭代过程中更改集合,它自然会引发异常。