为什么这段代码会抛出“集合被修改”,但是当我迭代之前的内容时却没有呢?


102
var ints = new List< int >( new[ ] {
    1,
    2,
    3,
    4,
    5
} );
var first = true;
foreach( var v in ints ) {
    if ( first ) {
        for ( long i = 0 ; i < int.MaxValue ; ++i ) { //<-- The thing I iterate
            ints.Add( 1 );
            ints.RemoveAt( ints.Count - 1 );
        }
        ints.Add( 6 );
        ints.Add( 7 );
    }
    Console.WriteLine( v );
    first = false;
}

如果注释掉内部for循环,则会引发该循环,这显然是因为我们对集合进行了更改。

现在,如果您取消注释,为什么此循环允许我们添加这两项?运行它大约需要半分钟(在奔腾CPU上),但它不会抛出,有趣的是它输出:

图片

有点期望,但是它表明我们可以更改,并且实际上可以更改集合。任何想法为什么这种行为发生?


2
那很有意思。我可以重现该行为,但是如果我将Int.MaxValue的内部循环更改为类似100的值,则无法复制
Steve

你等了多久?完成int.MaxValue迭代需要花一些时间...
Jon Skeet 2014年

1
我相信foreach会检查集合是否在每个循环的开始都已被修改....因此在每个循环中添加然后删除该项目不会引发任何错误。
卡兹(Kaz)2014年

6
您可以通过查看参考源并查看更改检测的工作原理来自己回答这个问题。并非所有人都知道参考文献的存在,只是散布了一个词:)
Christopher Currens 2014年

2
出于好奇:您是否在实际代码中遇到过此问题?
ken2k 2014年

Answers:


119

问题在于,List<T>检测修改的方式是保留一个type的version字段,int并在每次修改时对其进行递增。因此,如果您在两次迭代之间对列表进行了2 32次修改的精确倍数,那么就检测而言,这些修改将不可见。(它将从溢出到最终返回其初始值。)int.MaxValueint.MinValue

如果您对代码进行了几乎任何更改-添加1或3个值而不是2个值,或者将内部循环的迭代次数减少1个,则它将按预期方式引发异常。

(这是一个实现细节,而不是指定的行为-这是一个实现细节,在非常罕见的情况下,可以将其视为错误。但是,在实际程序中看到它会引起问题是非常不寻常的。)


5
仅供参考:相关源代码,请注意该_version字段是int
Lucas Trzesniewski

1
是的,它的设置正确,以便for循环完成后,_version的值为-2...。然后将6和7相加将其设置为0,使列表看起来像是未修改的。
卡兹(Kaz)2014年

4
我不确定这是否应称为“实施细节”,因为该实施决策有一个副作用,即即使不太可能发生,也是真实的。规范(或至少是文档)说应该抛出InvalidOperationException,实际上并非总是如此。当然,这取决于“实施细节”的定义。
ken2k 2014年

3
Jon Skeet,您是编程语言设计师吗?(在Google上没有找到任何相关的东西)有点奇怪为什么您也有此知识。这个问题有点讽刺了Stack Overflow的“功能”。
LyingOnTheSky 2014年

6
@LyingOnTheSky:不,尽管我喜欢扮演一名语言设计师,关注和批评C#语言。我也是ECMA-334技术小组的标准化C#5 ...,所以我可以挖洞,但不能做真正的语言设计工作:)
Jon Skeet 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.