Como dicen, el diablo está en los detalles ...
La mayor diferencia entre los dos métodos de enumeración de colecciones es que foreach
lleva el estado, mientras ForEach(x => { })
que no.
Pero profundicemos un poco más, porque hay algunas cosas que debe tener en cuenta que pueden influir en su decisión, y hay algunas advertencias que debe tener en cuenta al codificar para cualquier caso.
Usemos List<T>
en nuestro pequeño experimento para observar el comportamiento. Para este experimento, estoy usando .NET 4.7.2:
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
Vamos a iterar sobre esto con foreach
primero:
foreach (var name in names)
{
Console.WriteLine(name);
}
Podríamos ampliar esto en:
using (var enumerator = names.GetEnumerator())
{
}
Con el enumerador en la mano, mirando debajo de las cubiertas obtenemos:
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;
}
}
Dos cosas se hacen evidentes de inmediato:
- Se nos devuelve un objeto con estado con conocimiento íntimo de la colección subyacente.
- La copia de la colección es una copia superficial.
Por supuesto, esto no es seguro para subprocesos. Como se señaló anteriormente, cambiar la colección mientras se itera es un mal mojo.
Pero, ¿qué pasa con el problema de que la colección se vuelva inválida durante la iteración por medios ajenos a la manipulación de la colección durante la iteración? Las mejores prácticas sugieren versionar la colección durante las operaciones y la iteración, y verificar versiones para detectar cuándo cambia la colección subyacente.
Aquí es donde las cosas se ponen realmente turbias. De acuerdo con la documentación de Microsoft:
Si se realizan cambios en la colección, como agregar, modificar o eliminar elementos, el comportamiento del enumerador no está definido.
Bueno, qué significa eso? A modo de ejemplo, solo porque List<T>
implementa el manejo de excepciones no significa que todas las colecciones que implementan IList<T>
harán lo mismo. Eso parece ser una clara violación del Principio de sustitución de Liskov:
Los objetos de una superclase serán reemplazables por objetos de sus subclases sin interrumpir la aplicación.
Otro problema es que el enumerador debe implementar IDisposable
, lo que significa otra fuente de posibles pérdidas de memoria, no solo si la persona que llama se equivoca, sino si el autor no implementaDispose
patrón correctamente.
Por último, tenemos un problema de por vida ... ¿qué sucede si el iterador es válido, pero la colección subyacente se ha ido? Ahora tenemos una instantánea de lo que era ... cuando separas la vida útil de una colección y sus iteradores, estás pidiendo problemas.
Ahora examinemos ForEach(x => { })
:
names.ForEach(name =>
{
});
Esto se expande a:
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 es la siguiente:
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
Este código no asigna ningún enumerador (nada a Dispose
), y no se detiene mientras itera.
Tenga en cuenta que esto también realiza una copia superficial de la colección subyacente, pero la colección ahora es una instantánea en el tiempo. Si el autor no implementa correctamente una comprobación para que la colección cambie o quede "obsoleta", la instantánea sigue siendo válida.
Esto de ninguna manera lo protege del problema de los problemas de por vida ... si la colección subyacente desaparece, ahora tiene una copia superficial que señala lo que fue ... pero al menos no tiene un Dispose
problema para tratar con iteradores huérfanos ...
Sí, dije iteradores ... a veces es ventajoso tener estado. Suponga que desea mantener algo similar a un cursor de base de datos ... tal vez el foreach
estilo Iterator<T>
a seguir sea el de varios estilos . Personalmente, no me gusta este estilo de diseño, ya que hay demasiados problemas de por vida, y confías en las buenas gracias de los autores de las colecciones en las que confías (a menos que literalmente escribas todo tú mismo desde cero).
Siempre hay una tercera opción ...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
No es sexy, pero tiene dientes (disculpas a Tom Cruise y la película The Firm )
Es su elección, pero ahora lo sabe y puede ser informado.