Por que esse código exibe 'Coleção foi modificada', mas quando itero algo antes disso, isso não acontece?

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

Se você comentar o forloop interno , ele lançará, obviamente é porque fizemos alterações na coleção.

Agora, se você descomentá-lo, por que esse loop nos permite adicionar esses dois itens? Demora um pouco para executá-lo como meio minuto (na CPU Pentium), mas ele não joga, e o engraçado é que ele produz:

Imagem

Foi um pouco esperado, mas indica que podemos mudar e realmente muda a coleção. Alguma ideia de por que esse comportamento está ocorrendo?

LyingOnTheSky
fonte
2
Isso é interessante. Eu poderia reproduzir o comportamento, mas não se alterasse o loop interno de Int.MaxValue para um valor como 100
Steve
Por quando tempo você esperou? Demora um pouco para terminar as int.MaxValueiterações ...
Jon Skeet
1
Acredito que o foreach verifica se a coleção foi modificada no início de cada loop ... portanto, adicionar e remover o item em cada loop não gera erros.
Kaz
6
Você pode ter sido capaz de responder a essa pergunta olhando a fonte de referência e vendo como a detecção de alterações funcionava. Nem todo mundo sabe que a fonte de referência existe, apenas espalhando a palavra :)
Christopher Currens
2
Só por curiosidade: você teve esse problema em um trecho de código do mundo real?
ken2k

Respostas:

119

O problema é que a forma de List<T>detectar modificações é mantendo um campo de versão, do tipo int, incrementando-o a cada modificação. Portanto, se você fez exatamente algum múltiplo de 2 32 modificações na lista entre as iterações, isso tornará essas modificações invisíveis no que diz respeito à detecção. (Ele irá estourar de int.MaxValuepara int.MinValuee, eventualmente, voltar ao seu valor inicial.)

Se você alterar praticamente qualquer coisa em seu código - adicione 1 ou 3 valores em vez de 2, ou diminua o número de iterações de seu loop interno em 1, ele lançará uma exceção conforme o esperado.

(Este é um detalhe de implementação em vez de um comportamento especificado - e é um detalhe de implementação que pode ser observado como um bug em um caso muito raro. Seria muito incomum vê-lo causar um problema em um programa real, no entanto.)

Jon Skeet
fonte
5
Apenas para referência: código-fonte relevante , observe que o _versioncampo é um int.
Lucas Trzesniewski
1
Sim, está configurado corretamente para que, após o término do loop for, _version tenha um valor de -2 .... então, adicionar 6 e 7 coloca-o em 0, fazendo com que a lista pareça inalterada.
Kaz
4
Não tenho certeza se isso deve ser chamado de "detalhe de implementação", porque há um efeito colateral dessa decisão de implementação, que mesmo que seja improvável de acontecer, é real. A especificação (ou pelo menos o documento) diz que deve lançar um InvalidOperationException, o que nem sempre é verdade. Claro que isso depende da definição de "detalhes de implementação".
ken2k
3
Jon Skeet, você é designer de linguagem de programação? (Não encontrei nada relacionado no Google) Um pouco curioso por que você também tem esse conhecimento. Esta pergunta foi um pouco provocadora para ver o "poder" de Stack Overflow.
LyingOnTheSky
6
@LyingOnTheSky: Não, embora eu goste de brincar de ser um designer de linguagem em termos de seguir e criticar a linguagem C #. Eu também estou no grupo técnico ECMA-334 para padronizar C # 5 ... então eu pego buracos, mas não faço o trabalho real de design de linguagem :)
Jon Skeet