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;
}

If you comment out the inner for loop, it throws, it's obviously because we did changes to the collection.

Now if you uncomment it, why this loop allow us to add those two items? It takes awhile to run it like half a minute (On Pentium CPU), but it doesn't throw, and the funny thing is that it outputs:

Image

It was a bit of expected, but it indicates that we can change and it actually changes the collection. Any ideas why this behaviour occuring?

LyingOnTheSky
  • 2,844
  • 1
  • 14
  • 33
  • 2
    That's interesting. I could reproduce the behaviour, but not if I change the internal loop from Int.MaxValue to a value like 100 – Steve Nov 03 '14 at 16:58
  • How long did you wait? It takes quite a while to finish `int.MaxValue` iterations... – Jon Skeet Nov 03 '14 at 17:00
  • @JonSkeet I said it below the code, half a minute. (On pentium CPU) – LyingOnTheSky Nov 03 '14 at 17:01
  • @JonSkeet 15-20 seconds for me no more. But the op is right, this is working ! Also Steve is right too, I tried first with 10 and exception was thrown ! – mybirthname Nov 03 '14 at 17:01
  • 1
    I believe the foreach checks to see if the collection has been modified at the beginning of each loop....so adding and then removing the item within each loop doesn't throw any errors. – Kaz Nov 03 '14 at 17:02
  • @LyingOnTheSky: Hmm... it takes considerably longer than that for me, on a Core i7... still, I know what's going on now... – Jon Skeet Nov 03 '14 at 17:02
  • @Kazmatron But why adding two items after that, and it actually adds them? That's the funny part, look at the output. It modifies only on the first iteration. – LyingOnTheSky Nov 03 '14 at 17:03
  • @LyingOnTheSky When I change the for loop limit to 100, I get an error when adding "6" and then "7" to the list....as I would expect. (not waiting for int.max lol) – Kaz Nov 03 '14 at 17:06
  • 6
    You might have been able to answer this question yourself by looking at the [reference source](http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs#9c3d580a8b7a8fe8#references) and seeing how change detection worked. Not everyone knows the reference source even exists, just spreading the word :) – Christopher Currens Nov 03 '14 at 17:06
  • 2
    Just out of curiosity: did you had this issue in a real-world piece of code? – ken2k Nov 03 '14 at 17:15

1 Answers1

121

The problem is that the way that List<T> detects modifications is by keeping a version field, of type int, incrementing it on each modification. Therefore, if you've made exactly some multiple of 232 modifications to the list between iterations, it will render those modifications invisible as far as detection is concerned. (It will overflow from int.MaxValue to int.MinValue and eventually get back to its initial value.)

If you change pretty much anything about your code - add 1 or 3 values rather than 2, or lower the number of iterations of your inner loop by 1, then it will throw an exception as expected.

(This is an implementation detail rather than specified behaviour - and it's an implementation detail which can be observed as a bug in a very rare case. It would be very unusual to see it cause a problem in a real program, however.)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 5
    Just for reference: [relevant source code](http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs#1224), note that the `_version` field is an `int`. – Lucas Trzesniewski Nov 03 '14 at 17:07
  • 1
    Yup, it's set up just right so that after the for loop finishes, _version has a value of -2....then adding 6 and 7 puts it to 0, making the list look like it's unmodified. – Kaz Nov 03 '14 at 17:18
  • 4
    I'm not sure this should be called an "implementation detail", because there is a side effect of that implementation decision, that even if unlikely to happen, is real. The spec (or at least the doc) says it should throw an `InvalidOperationException`, which is actually not always true. Of course this depends on the definition of "implementation detail". – ken2k Nov 03 '14 at 17:25
  • @ken2k: It's definitely an observable implementation detail - but it's not specified behaviour. Will clarify. – Jon Skeet Nov 03 '14 at 17:39
  • That's why I use 64 bit version counters in code that relies on being able to detect modifications via a counter (e.g. to determine if a document has been modified since the last save). – CodesInChaos Nov 03 '14 at 19:45
  • that's why it's called fail-fast not fail-safe; they are not perfect – ratchet freak Nov 03 '14 at 21:45
  • 1
    @EricJ have a look at Jon's profile, then look [here](http://meta.stackexchange.com/questions/9134/jon-skeet-facts) for an overview of his awesome powers. :) – brichins Nov 05 '14 at 15:47
  • 1
    @brichins: Yes, I'm well aware of Jon's many accomplishments. His depth of knowledge still amazes me. – Eric J. Nov 05 '14 at 17:58
  • 3
    Jon Skeet, are you programming language designer? (Didn't found anything-related on Google) A bit curious why you have this knowledge too. This question was a bit of a tease to see Stack Overflow's "power". – LyingOnTheSky Nov 06 '14 at 12:25
  • 6
    @LyingOnTheSky: Nope, although I like to play at being a language designer in terms of following and critiquing the C# language. I'm also on the ECMA-334 technical group for standardizing C# 5... so I get to pick holes but not do the real language design work :) – Jon Skeet Nov 06 '14 at 13:03