Por que List <T> .ForEach permite que sua lista seja modificada?

90

Se eu usar:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

o Addin foreachlança uma InvalidOperationException (Collection foi modificada; a operação de enumeração pode não ser executada), que considero lógico, já que estamos puxando o tapete debaixo de nossos pés.

No entanto, se eu usar:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

ele prontamente atira no próprio pé fazendo um loop até que lance uma OutOfMemoryException.

Isso é uma surpresa para mim, pois sempre pensei que List.ForEach era apenas um invólucro para foreachou para for.
Alguém tem uma explicação para como e por que esse comportamento?

(Inspirado por ForEach loop para uma lista genérica repetida indefinidamente )

SWeko
fonte
7
Concordo. Isso é - duvidoso. Sugiro que você poste isso no microsoft connect e peça esclarecimentos.
TomTom
4
"Isso é uma surpresa para mim, pois sempre pensei que List.ForEach era apenas um invólucro para foreachou para for." Ainda pode usar for. Você pode executar a mesma ação em um forloop e gerar a mesma OutOfMemoryException como resultado.
Anthony Pegram
Isso é baseado na minha pergunta: stackoverflow.com/q/9311272/132239 , obrigado SWeko por entrar em seus detalhes
Kasrak

Respostas:

68

É porque o ForEachmétodo não usa o enumerador, ele percorre os itens com um forloop:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(código obtido com JustDecompile)

Como o enumerador não é usado, ele nunca verifica se a lista foi alterada e a condição final do forloop nunca é alcançada porque _sizeé aumentada a cada iteração.

Thomas Levesque
fonte
Sim, mas como é _sizecalculado? Se for apenas pré-calculado, deve ser executado apenas uma vez para o meu exemplo. É obviamente atualizado de alguma forma.
SWeko de
7
Ele é atualizado em Adicionar método -> this._items [this._size ++] = item;
Fabio
1
@SWeko, não é calculado, é atualizado toda vez que um item é adicionado ou removido.
Thomas Levesque de
1
Existe uma _versionvariável privada em List<T>que pode detectar este tipo de cenário, pois é atualizada nas operações que alteram a própria lista.
SWeko de
Você poderia evitar exceções obtendo primeiro o tamanho (int theSize = this._size) e, em seguida, usando-o no loop for?
Lazlow
14

List<T>.ForEaché implementado através de fordentro, por isso não usa enumerador e permite modificar a coleção.

Alexey Raga
fonte
6

Porque o ForEach anexado à classe List usa internamente um loop for que é anexado diretamente a seus membros internos - que você pode ver baixando o código-fonte do .NET framework.

http://referencesource.microsoft.com/netframework.aspx

Onde, como um loop foreach, é antes de tudo uma otimização do compilador, mas também deve operar contra a coleção como um observador - então, se a coleção for modificada, ele lançará uma exceção.

Mike Perrenoud
fonte
E para responder ao comentário no post do @Thomas sobre como ele se atualiza - os membros internos são atualizados quando add é chamado, por isso é capaz de acompanhar as mudanças. Se você fosse realizar uma inserção, em um índice menor que o atual, você nunca operaria naquele item porque ele já foi iterado após aquele item. Mas, como você está adicionando no final, funciona.
Mike Perrenoud
1
Sim, alterar a Addlinha strings.Insert(0, s + "!")apenas imprime 'amostra'. É estranho que isso não seja mencionado na documentação.
SWeko de
Bem, acho que a Microsoft percebeu que é quase impossível fornecer todas as advertências existentes em sua documentação - então, eles fornecem seu código-fonte agora. Acho que essa é uma solução melhor, honestamente, mas o único problema que encontrei é que produtos como o WF não são atualizados tão rapidamente - o código-fonte do WF 4.x ainda não está disponível.
Mike Perrenoud
4

Nós sabemos sobre este problema, foi um descuido quando foi originalmente escrito. Infelizmente, não podemos alterá-lo porque agora impediria a execução desse código que funcionava anteriormente:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

A utilidade desse método em si é questionável, como Eric Lippert apontou, então não o incluímos para .NET para aplicativos estilo Metro (ou seja, aplicativos do Windows 8).

David Kean (Equipe BCL)

David Kean
fonte
1
Vejo que essa seria uma grande mudança de ruptura ruim, mas mesmo assim pode falhar de maneiras não óbvias e isso nunca é uma coisa boa. Não consigo ver um cenário em que o uso do método ForEach seja superior a um for simples (ou foreach se a alteração da lista original não for necessária)
SWeko