foreach vs someList.ForEach () {}


167

Aparentemente, existem várias maneiras de interagir com uma coleção. Curioso se houver alguma diferença, ou por que você usaria um caminho sobre o outro.

Primeiro tipo:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Outro jeito:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Suponho que, em vez de um delegado anônimo que eu uso acima, você tenha um delegado reutilizável que possa especificar ...


Respostas:


134

Há uma distinção importante e útil entre os dois.

Como o .ForEach usa um forloop para iterar a coleção, isso é válido (edit: anterior ao .net 4.5 - a implementação foi alterada e os dois lançaram):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

Considerando que foreachusa um enumerador, então isso não é válido:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: NÃO copypaste este código no seu aplicativo!

Esses exemplos não são práticas recomendadas, são apenas para demonstrar as diferenças entre ForEach()e foreach.

Remover itens de uma lista dentro de um forloop pode ter efeitos colaterais. O mais comum é descrito nos comentários a esta pergunta.

Geralmente, se você deseja remover vários itens de uma lista, você deseja separar a determinação de quais itens remover da remoção real. Ele não mantém seu código compacto, mas garante que você não perca nenhum item.


35
mesmo assim, você deve usar someList.RemoveAll (x => x.RemoveMe)
Mark Cidade

2
Com o Linq, tudo pode ser feito melhor. Eu estava mostrando um exemplo de modificar a coleção dentro foreach ...

2
RemoveAll () é um método na Lista <T>.
Mark Cidade

3
Você provavelmente está ciente disso, mas as pessoas devem ter cuidado ao remover itens dessa maneira; se você remover o item N, a iteração pulará o item (N + 1) e você não o verá no seu delegado nem terá a chance de removê-lo, como se você tivesse feito isso sozinho para o loop.
El Zorko

9
Se você iterar a lista para trás com um loop for, você pode remover itens sem problemas índice de deslocamento.
S. Tarik Çetin

78

Tínhamos algum código aqui (no VS2005 e C # 2.0) onde os engenheiros anteriores se esforçavam para usar em list.ForEach( delegate(item) { foo;});vez de foreach(item in list) {foo; };todo o código que eles escreviam. por exemplo, um bloco de código para ler linhas de um dataReader.

Ainda não sei exatamente por que eles fizeram isso.

As desvantagens de list.ForEach()são:

  • É mais detalhado no C # 2.0. No entanto, no C # 3 em diante, você pode usar a =>sintaxe " " para criar expressões bem concisas.

  • É menos familiar. As pessoas que precisam manter esse código se perguntam por que você fez dessa maneira. Levei um tempo para decidir que não havia motivo, exceto talvez para fazer o escritor parecer inteligente (a qualidade do restante do código prejudicou isso). Também era menos legível, com o " })" no final do bloco de código delegado.

  • Veja também o livro de Bill Wagner "C # eficaz: 50 maneiras específicas de melhorar seu C #", onde ele fala sobre por que o foreach é preferido a outros loops como for ou while - o ponto principal é que você está deixando o compilador decidir a melhor maneira de construir o laço. Se uma versão futura do compilador conseguir usar uma técnica mais rápida, você a receberá gratuitamente usando foreach e reconstrução, em vez de alterar seu código.

  • uma foreach(item in list)construção permite que você use breakou continuese precisar sair da iteração ou do loop. Mas você não pode alterar a lista dentro de um loop foreach.

Estou surpreso ao ver que list.ForEaché um pouco mais rápido. Mas esse provavelmente não é um motivo válido para usá-lo, seria otimização prematura. Se o seu aplicativo usar um banco de dados ou serviço da Web que, não o controle de loop, quase sempre será onde o tempo passa. E você também comparou isso com um forloop? A list.ForEachpoderia ser mais rápido devido ao uso que, internamente e um forlaço sem o invólucro seria ainda mais rápido.

Não concordo que a list.ForEach(delegate)versão seja "mais funcional" de forma significativa. Ele passa uma função para uma função, mas não há grande diferença no resultado ou na organização do programa.

Eu não acho que foreach(item in list)"diz exatamente como você quer que seja feito" - um for(int 1 = 0; i < count; i++)loop faz isso, umforeach loop deixa a escolha do controle até o compilador.

Meu sentimento é, em um novo projeto, usar foreach(item in list)a maioria dos loops para aderir ao uso comum e à legibilidade, e usar list.Foreach()apenas para blocos curtos, quando você pode fazer algo mais elegante ou compacto com o =>operador C # 3 " ". Em casos como esse, já pode haver um método de extensão LINQ mais específico que ForEach(). Veja se Where(), Select(), Any(), All(), Max()ou um dos muitos outros métodos LINQ não já fazer o que quiser a partir do loop.


Só por curiosidade ... veja a implementação da Microsoft ... referenceource.microsoft.com/#mscorlib/system/collections/…
Alexandre

17

Por diversão, coloquei List no refletor e este é o C # resultante:

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

Da mesma forma, o MoveNext no Enumerador que é usado pelo foreach é o seguinte:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

O List.ForEach é muito mais reduzido do que o MoveNext - muito menos processamento - provavelmente JIT em algo eficiente.

Além disso, foreach () alocará um novo enumerador, não importa o quê. O GC é seu amigo, mas se você estiver fazendo o mesmo repetidamente, isso criará mais objetos descartáveis, em vez de reutilizar o mesmo delegado - MAS - esse é realmente um caso marginal. No uso típico, você verá pouca ou nenhuma diferença.


1
Você não tem garantia de que o código gerado pelo foreach seja o mesmo entre as versões do compilador. O código gerado pode ser aprimorado por uma versão futura.
22310 Anthony

1
A partir do .NET Core 3.1 ForEach ainda é mais rápido.
jackmott 31/01

13

Conheço duas coisas obscuras que as tornam diferentes. Vai eu!

Em primeiro lugar, existe o bug clássico de criar um delegado para cada item da lista. Se você usar a palavra-chave foreach, todos os seus representantes poderão acabar se referindo ao último item da lista:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

O método List.ForEach não tem esse problema. O item atual da iteração é passado por valor como um argumento para o lambda externo e, em seguida, o lambda interno captura corretamente esse argumento em seu próprio fechamento. Problema resolvido.

(Infelizmente, acredito que o ForEach é membro da List, e não um método de extensão, embora seja fácil defini-lo para que você tenha esse recurso em qualquer tipo enumerável.)

Em segundo lugar, a abordagem do método ForEach tem uma limitação. Se você estiver implementando IEnumerable usando retorno de rendimento, não poderá fazer um retorno de rendimento dentro do lambda. Portanto, percorrer os itens de uma coleção para gerar retornos não é possível por esse método. Você precisará usar a palavra-chave foreach e contornar o problema de fechamento fazendo uma cópia manualmente do valor atual do loop dentro do loop.

Mais aqui


5
O problema que você mencionou foreaché "corrigido" no C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior

13

Acho que a someList.ForEach()chamada pode ser facilmente paralelizada, enquanto o normal foreachnão é tão fácil de executar em paralelo. Você poderia executar facilmente vários delegados diferentes em núcleos diferentes, o que não é tão fácil de fazer com um normal foreach.
Apenas meus 2 centavos


2
Eu acho que ele quis dizer que o mecanismo de tempo de execução poderia paralelizar automaticamente. Caso contrário, tanto foreach e .ForEach pode ser paralelizado à mão usando um thread do pool em cada delegado ação
Isak Savo

@Isak, mas isso seria uma suposição incorreta. Se o método anônimo elevar um local ou membro, o tempo de execução não será fácil) ser capaz de paralelizar
Rune FS

6

Como se costuma dizer, o diabo está nos detalhes ...

A maior diferença entre os dois métodos de enumeração de coleção é que foreachcarrega state, enquanto ForEach(x => { })que não.

Mas vamos nos aprofundar um pouco mais, porque há algumas coisas que você deve estar ciente que podem influenciar sua decisão e há algumas ressalvas que você deve estar ciente ao codificar para qualquer um dos casos.

Vamos usar List<T>em nosso pequeno experimento para observar o comportamento. Para esta experiência, estou usando o .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Permite iterar sobre isso com o foreachprimeiro:

foreach (var name in names)
{
    Console.WriteLine(name);
}

Poderíamos expandir isso para:

using (var enumerator = names.GetEnumerator())
{

}

Com o enumerador em mãos, olhando embaixo das cobertas, obtemos:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

Duas coisas se tornam imediatamente evidentes:

  1. Retornamos um objeto com estado com conhecimento íntimo da coleção subjacente.
  2. A cópia da coleção é uma cópia superficial.

Naturalmente, isso não é de forma alguma seguro para threads. Como foi apontado acima, alterar a coleção durante a iteração é apenas um mojo ruim.

Mas e o problema de a coleção se tornar inválida durante a iteração por meios fora de nós que mexem com a coleção durante a iteração? As práticas recomendadas sugerem a versão da coleção durante operações e iteração e a verificação de versões para detectar quando a coleção subjacente é alterada.

Aqui é onde as coisas ficam realmente sombrias. De acordo com a documentação da Microsoft:

Se forem feitas alterações na coleção, como adicionar, modificar ou excluir elementos, o comportamento do enumerador será indefinido.

Bem, o que isso significa? A título de exemplo, apenas porque List<T>implementa o tratamento de exceções não significa que todas as coleções que implementam IList<T>farão o mesmo. Isso parece ser uma clara violação do Princípio da Substituição de Liskov:

Os objetos de uma superclasse devem ser substituíveis pelos objetos de suas subclasses sem interromper o aplicativo.

Outro problema é que o enumerador deve implementar IDisposable- isso significa outra fonte de vazamentos de memória em potencial, não apenas se o chamador errar, mas se o autor não implementar oDispose padrão corretamente.

Por fim, temos um problema vitalício ... o que acontece se o iterador for válido, mas a coleção subjacente desaparecer? Agora, temos um instantâneo do que era ... quando você separa a vida útil de uma coleção e seus iteradores, você está pedindo problemas.

Vamos agora examinar ForEach(x => { }):

names.ForEach(name =>
{

});

Isso se expande para:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

De nota importante é o seguinte:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Esse código não aloca nenhum enumerador (nada a Dispose) e não pausa durante a iteração.

Observe que isso também executa uma cópia superficial da coleção subjacente, mas a coleção agora é um instantâneo no tempo. Se o autor não implementar corretamente uma verificação para a coleção mudar ou ficar obsoleta, o instantâneo ainda será válido.

Isso não protege de maneira alguma o problema dos problemas da vida útil ... se a coleção subjacente desaparecer, agora você terá uma cópia superficial que aponta para o que era ... mas pelo menos você não tem uma Dispose problemas para lidar com iteradores órfãos ...

Sim, eu disse iteradores ... às vezes é vantajoso ter estado. Suponha que você queira manter algo semelhante a um cursor de banco de dados ... talvez vários foreachestilos Iterator<T>sejam o caminho a seguir. Pessoalmente, não gosto desse estilo de design, pois há muitos problemas ao longo da vida e você confia nas boas graças dos autores das coleções nas quais está confiando (a menos que você literalmente escreva tudo do zero).

Sempre há uma terceira opção ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Não é sexy, mas tem dentes (desculpas a Tom Cruise e ao filme The Firm )

É sua escolha, mas agora você sabe e pode ser informado.


5

Você pode nomear o delegado anônimo :-)

E você pode escrever o segundo como:

someList.ForEach(s => s.ToUpper())

O que eu prefiro e economiza muita digitação.

Como Joachim diz, o paralelismo é mais fácil de aplicar à segunda forma.


2

Nos bastidores, o delegado anônimo é transformado em um método real, para que você possa ter alguma sobrecarga com a segunda opção se o compilador não optar por incorporar a função. Além disso, quaisquer variáveis ​​locais referenciadas pelo corpo do exemplo de delegado anônimo mudariam de natureza devido a truques do compilador para ocultar o fato de que ela é compilada para um novo método. Mais informações aqui sobre como o C # faz essa mágica:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

A função ForEach é membro da classe genérica List.

Eu criei a seguinte extensão para reproduzir o código interno:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Então, no final, estamos usando um foreach normal (ou um loop, se você quiser).

Por outro lado, o uso de uma função delegada é apenas outra maneira de definir uma função, este código:

delegate(string s) {
    <process the string>
}

é equivalente a:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

ou usando expressões labda:

(s) => <process the string>

2

Todo o escopo ForEach (delegar função) é tratado como uma única linha de código (chamando a função) e você não pode definir pontos de interrupção nem entrar no código. Se ocorrer uma exceção não tratada, o bloco inteiro será marcado.


2

List.ForEach () é considerado mais funcional.

List.ForEach()diz o que você quer fazer. foreach(item in list)também diz exatamente como você quer que seja feito. Isso deixa List.ForEachlivre para alterar a implementação da parte como no futuro. Por exemplo, uma versão futura hipotética do .Net sempre pode ser executada List.ForEachem paralelo, supondo que neste momento todos tenham um número de núcleos de CPU que geralmente estão ociosos.

Por outro lado, foreach (item in list)oferece um pouco mais de controle sobre o loop. Por exemplo, você sabe que os itens serão iterados em algum tipo de ordem seqüencial e poderá facilmente interromper no meio se um item atender a alguma condição.


Algumas observações mais recentes sobre esse assunto estão disponíveis aqui:

https://stackoverflow.com/a/529197/3043


1

A segunda maneira que você mostrou usa um método de extensão para executar o método delegado para cada um dos elementos da lista.

Dessa forma, você tem outra chamada de delegado (= método).

Além disso, existe a possibilidade de iterar a lista com um loop for .


1

Uma coisa a ser cautelosa é como sair do método .ForEach genérico - consulte esta discussão . Embora o link pareça dizer que esse caminho é o mais rápido. Não sei por que - você pensaria que eles seriam equivalentes uma vez compilados ...


0

Existe uma maneira, eu fiz isso no meu aplicativo:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Você pode usar o item como do foreach

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.