为什么List<T>.ForEach()实现了一个for循环?

12
我不明白为什么 List<T>.ForEach() 扩展方法在内部实现了一个 for 循环。这会导致可能修改集合。普通的 foreach 在这种情况下会抛出异常,那么 ForEach() 应该同样做出反应才对吧?
如果你必须改变集合,那么肯定要手动使用 for 循环来迭代集合吧? foreachList<T>.ForEach() 之间似乎存在一些语义上的矛盾。
我有什么遗漏吗?

5
这打开了修改集合的可能性。这也是为什么它现在从.NET的Metro风格应用程序中消失的确切原因。嗯。 - BoltClock
3
在这个情境中,Eric Lippert(不满的)对于.ForEach的评论值得一读。 - Kirk Woll
顺便说一下:https://dev59.com/7Gkv5IYBdhLWcg3w3Uc0 - user287107
@user287107:那就是我不久前链接的内容。 - BoltClock
1
...... 这就是如果你改变数据时会发生的事情 - 抱歉LINQ具有深层的FP根源,这就是为什么不可变数据和参考透明度在FP中被鼓励的原因... 如果你不喜欢实现方式,那就自己创造一个 - 或者更好的是接受不可变数据! - Random Dev
3个回答

5
只有BCL团队的成员才能确定,但List<T>.ForEach让你修改列表可能只是疏忽大意。
首先,David B的回答对我来说没有意义。它检查的是List<T>是否在foreach循环中修改了列表,并且如果您这样做,会抛出InvalidOperationException。它与您使用的语言无关。
其次,在文档中有这个警告:
“在Action<T>委托主体中修改基础集合不受支持并导致未定义的行为。”
我认为BCL团队不希望像ForEach这样的简单方法具有未定义的行为。
第三,在.NET 4.5中,List<T>.ForEach如果委托修改列表将会抛出InvalidOperationException。如果程序依赖旧行为,在重新编译为.NET 4.5时将停止工作。Microsoft愿意接受这种破坏性变化的事实强烈表明,原始行为是不被预期的,不能依赖它。
供参考,这是.NET 4.0中List<T>.ForEach的实现,直接来自参考源:
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]);
    }
}

以下是在.NET 4.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);
}

@nawfal:可能不需要。检查 if (version != _version) 的成本不太可能是可测量的。 - Michael Liu
实际上,文档记录了如果列表被修改甚至是“调用集合中的元素被修改”,都会抛出异常。这太疯狂了。为什么要故意破坏一种良性行为呢? - Suncat2000

5
因为 List.ForEach 遵循 MSDN 的定义:

对 List 的每个元素执行指定操作。

这意味着执行在元素上的 Action 可能会改变元素本身,或者集合本身。在这种情况下,除了使用简单的 for 外,没有其他方法(如果不是创建代价昂贵的克隆集合,如果可能的话)。
如果在 foreach 迭代期间更改集合,则自然会引发异常。

1
啊,按照定义来说是正确的。但我仍觉得它有点误导人。我想这就是所有争议的原因(也是BoltClock指出他们将其从.NET Metro中删除的原因)? - Dave New
2
@davenewza 不,链接的文档说“在Action<T>委托的主体中修改底层集合不受支持并且会导致未定义的行为。”因此,他们说您不应该在action委托中修改List<> - Jeppe Stig Nielsen

5
foreach是C#语言的一个元素,它遵循C#语言的规则。 List<T>.ForEach是.NET Framework的一个方法,它遵循.NET Framework的规则,在这个规则中,foreach不存在。
这是“语言与框架”混淆的一个例子。框架方法必须在许多语言中工作,并且这些语言(通常)具有矛盾的语义。
另一个“语言与框架”混淆的例子是在.NET 3和.NET 3.5之间对Enumerable.Cast的破坏性更改。在.NET 3中,Cast使用了C#语义。在.NET 3.5中,它被改为使用.NET语义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接