¿Por qué este código arroja 'Colección fue modificada', pero cuando itero algo antes, no lo hace?

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

Si comenta el forbucle interno , arroja, obviamente es porque hicimos cambios en la colección.

Ahora, si lo descomentas, ¿por qué este bucle nos permite agregar esos dos elementos? Lleva un tiempo ejecutarlo como medio minuto (en CPU Pentium), pero no arroja, y lo curioso es que genera:

Imagen

Era un poco de lo esperado, pero indica que podemos cambiar y en realidad cambia la colección. ¿Alguna idea de por qué ocurre este comportamiento?

LyingOnTheSky
fuente
2
Eso es interesante. Podría reproducir el comportamiento, pero no si cambio el bucle interno de Int.MaxValue a un valor como 100
Steve
¿Cuánto esperaste? Se necesita bastante tiempo para terminar las int.MaxValueiteraciones ...
Jon Skeet
1
Creo que foreach verifica si la colección se ha modificado al comienzo de cada ciclo ... por lo que agregar y luego eliminar el elemento dentro de cada ciclo no arroja ningún error.
Kaz
6
Es posible que haya podido responder esta pregunta usted mismo mirando la fuente de referencia y viendo cómo funcionaba la detección de cambios. No todo el mundo sabe que la fuente de referencia existe, solo
corre la voz
2
Solo por curiosidad: ¿tuvo este problema en un fragmento de código del mundo real?
ken2k

Respuestas:

119

El problema es que la forma de List<T>detectar modificaciones es manteniendo un campo de versión, de tipo int, incrementándolo en cada modificación. Por lo tanto, si ha realizado exactamente un múltiplo de 2 32 modificaciones a la lista entre iteraciones, hará que esas modificaciones sean invisibles en lo que respecta a la detección. (Se desbordará a partir int.MaxValuede int.MinValuey, finalmente, volver a su valor inicial.)

Si cambia prácticamente cualquier cosa sobre su código, agregue 1 o 3 valores en lugar de 2, o reduzca el número de iteraciones de su ciclo interno en 1, entonces arrojará una excepción como se esperaba.

(Este es un detalle de implementación en lugar de un comportamiento especificado, y es un detalle de implementación que se puede observar como un error en un caso muy raro. Sin embargo, sería muy inusual ver que causa un problema en un programa real).

Jon Skeet
fuente
5
Solo como referencia: código fuente relevante , tenga en cuenta que el _versioncampo es un int.
Lucas Trzesniewski
1
Sí, está configurado correctamente para que después de que finalice el ciclo for, _version tenga un valor de -2 ... luego, al agregar 6 y 7, se pone a 0, haciendo que la lista parezca no modificada.
Kaz
4
No estoy seguro de que esto deba llamarse un "detalle de implementación", porque hay un efecto secundario de esa decisión de implementación, que incluso si es poco probable que suceda, es real. La especificación (o al menos el documento) dice que debería arrojar un InvalidOperationException, que en realidad no siempre es cierto. Por supuesto, esto depende de la definición de "detalle de implementación".
ken2k
3
Jon Skeet, ¿eres diseñador de lenguajes de programación? (No encontré nada relacionado con Google). Un poco curioso por qué usted también tiene este conocimiento. Esta pregunta fue un poco burlona para ver el "poder" de Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: No, aunque me gusta jugar a ser un diseñador de lenguaje en términos de seguir y criticar el lenguaje C #. También estoy en el grupo técnico ECMA-334 para estandarizar C # 5 ... así que puedo buscar agujeros pero no hacer el trabajo de diseño del lenguaje real :)
Jon Skeet