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 foreach
carrega 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 foreach
primeiro:
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:
- Retornamos um objeto com estado com conhecimento íntimo da coleção subjacente.
- 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 foreach
estilos 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.