Pourquoi ce code lance "La collection a été modifiée", mais quand j'itère quelque chose avant, ce n'est pas le cas?

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 vous commentez la forboucle interne , cela lève, c'est évidemment parce que nous avons apporté des modifications à la collection.

Maintenant, si vous le décommentez, pourquoi cette boucle nous permet-elle d'ajouter ces deux éléments? Cela prend un certain temps pour l'exécuter comme une demi-minute (sur un processeur Pentium), mais cela ne lance pas, et le plus drôle est qu'il génère:

Image

C'était un peu attendu, mais cela indique que nous pouvons changer et cela change réellement la collection. Des idées pourquoi ce comportement se produit?

LyingOnTheSky
la source
2
C'est intéressant. Je pourrais reproduire le comportement, mais pas si je change la boucle interne de Int.MaxValue à une valeur comme 100
Steve
Combien de temps avez-vous attendu? Il faut un certain temps pour terminer les int.MaxValueitérations ...
Jon Skeet
1
Je crois que le foreach vérifie si la collection a été modifiée au début de chaque boucle ... donc l'ajout puis la suppression de l'élément dans chaque boucle ne jette aucune erreur.
Kaz
6
Vous avez peut-être été en mesure de répondre vous-même à cette question en regardant la source de référence et en voyant comment la détection des changements fonctionnait. Tout le monde ne sait pas que la source de référence existe même, il suffit de passer le mot :)
Christopher Currens
2
Juste par curiosité: avez-vous eu ce problème dans un morceau de code réel?
ken2k

Réponses:

119

Le problème est que la manière de List<T>détecter les modifications consiste à conserver un champ de version, de type int, en l'incrémentant à chaque modification. Par conséquent, si vous avez fait exactement un multiple de 2 32 modifications à la liste entre les itérations, cela rendra ces modifications invisibles en ce qui concerne la détection. (Il débordera de int.MaxValuevers int.MinValueet finira par revenir à sa valeur initiale.)

Si vous modifiez à peu près n'importe quoi dans votre code - ajoutez 1 ou 3 valeurs au lieu de 2, ou réduisez le nombre d'itérations de votre boucle interne de 1, alors il lancera une exception comme prévu.

(Il s'agit d'un détail d'implémentation plutôt que d'un comportement spécifié - et c'est un détail d'implémentation qui peut être observé comme un bogue dans un cas très rare. Il serait cependant très inhabituel de le voir causer un problème dans un programme réel.)

Jon Skeet
la source
5
Juste pour référence: code source pertinent , notez que le _versionchamp est un int.
Lucas Trzesniewski
1
Oui, il est configuré correctement pour qu'après la fin de la boucle for, _version ait une valeur de -2 .... puis l'ajout de 6 et 7 le met à 0, ce qui donne l'impression que la liste n'a pas été modifiée.
Kaz
4
Je ne suis pas sûr que cela devrait être appelé un «détail de mise en œuvre», car il y a un effet secondaire de cette décision de mise en œuvre, qui, même s'il est peu probable, est réel. La spécification (ou du moins la doc) dit qu'elle devrait lancer un InvalidOperationException, ce qui n'est en fait pas toujours vrai. Bien entendu, cela dépend de la définition du "détail de mise en œuvre".
ken2k
3
Jon Skeet, êtes-vous concepteur de langage de programmation? (Je n'ai rien trouvé de lié sur Google) Je suis un peu curieux de savoir pourquoi vous avez aussi cette connaissance. Cette question était un peu taquine pour voir la "puissance" de Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: Non, même si j'aime jouer à être un concepteur de langage en termes de suivi et de critique du langage C #. Je fais également partie du groupe technique ECMA-334 pour la normalisation du C # 5 ... donc je peux choisir des trous mais pas faire le vrai travail de conception du langage :)
Jon Skeet