¿Cuál es la mejor manera de modificar una lista en un ciclo ‘foreach’?

Una nueva característica en C # / .NET 4.0 es que puedes cambiar tu enumerable en un foreach sin obtener la excepción. Consulte la entrada del blog de Paul Jackson. Un efecto secundario interesante de la concurrencia: eliminar elementos de una colección al enumerar para obtener información sobre este cambio.

¿Cuál es la mejor manera de hacer lo siguiente?

 foreach(var item in Enumerable) { foreach(var item2 in item.Enumerable) { item.Add(new item2) } } 

Por lo general, uso un IList como memoria caché / buffer hasta el final del foreach , pero ¿hay una mejor manera?

La colección utilizada en foreach es inmutable. Esto es mucho por diseño.

Como dice en MSDN :

La instrucción foreach se usa para recorrer la colección para obtener la información que desea, pero no se puede usar para agregar o eliminar elementos de la colección de origen para evitar efectos secundarios impredecibles. Si necesita agregar o quitar elementos de la colección fuente, use un ciclo for.

La publicación en el enlace proporcionado por Poko indica que esto está permitido en las nuevas colecciones concurrentes.

Haga una copia de la enumeración, usando un método de extensión IEnumerable en este caso, y enumere sobre él. Esto agregaría una copia de cada elemento en cada enumerable interno a esa enumeración.

 foreach(var item in Enumerable) { foreach(var item2 in item.Enumerable.ToList()) { item.Add(item2) } } 

Como se mencionó, pero con una muestra de código:

 foreach(var item in collection.ToArray()) collection.Add(new Item...); 

Para ilustrar la respuesta de Nippysaurus: si va a agregar los nuevos elementos a la lista y desea procesar los elementos recién agregados también durante la misma enumeración, puede usar for loop en lugar de foreach loop, problema resuelto 🙂

 var list = new List(); ... populate the list ... //foreach (var entryToProcess in list) for (int i = 0; i < list.Count; i++) { var entryToProcess = list[i]; var resultOfProcessing = DoStuffToEntry(entryToProcess); if (... condition ...) list.Add(new YourData(...)); } 

Para el ejemplo ejecutable:

 void Main() { var list = new List(); for (int i = 0; i < 10; i++) list.Add(i); //foreach (var entry in list) for (int i = 0; i < list.Count; i++) { var entry = list[i]; if (entry % 2 == 0) list.Add(entry + 1); Console.Write(entry + ", "); } Console.Write(list); } 

Salida del último ejemplo:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 3, 5, 7, 9,

Lista (15 artículos)
0
1
2
3
4
5
6
7
8
9
1
3
5
7
9

Así es como puede hacerlo (solución rápida y sucia. Si realmente necesita este tipo de comportamiento, debe reconsiderar su diseño o anular todos IList miembros de IList y agregar la lista de origen):

 using System; using System.Collections.Generic; namespace ConsoleApplication3 { public class ModifiableList : List { private readonly IList pendingAdditions = new List(); private int activeEnumerators = 0; public ModifiableList(IEnumerable collection) : base(collection) { } public ModifiableList() { } public new void Add(T t) { if(activeEnumerators == 0) base.Add(t); else pendingAdditions.Add(t); } public new IEnumerator GetEnumerator() { ++activeEnumerators; foreach(T t in ((IList)this)) yield return t; --activeEnumerators; AddRange(pendingAdditions); pendingAdditions.Clear(); } } class Program { static void Main(string[] args) { ModifiableList ints = new ModifiableList(new int[] { 2, 4, 6, 8 }); foreach(int i in ints) ints.Add(i * 2); foreach(int i in ints) Console.WriteLine(i * 2); } } } 

LINQ es muy efectivo para hacer malabarismos con colecciones.

Tus tipos y estructura no están claros para mí, pero intentaré adaptar tu ejemplo lo mejor que pueda.

Desde su código, parece que, para cada elemento, está agregando a ese elemento todo desde su propia propiedad ‘Enumerable’. Esto es muy simple:

 foreach (var item in Enumerable) { item = item.AddRange(item.Enumerable)); } 

Como ejemplo más general, digamos que queremos iterar una colección y eliminar elementos donde cierta condición es verdadera. Evitando foreach , usando LINQ:

 myCollection = myCollection.Where(item => item.ShouldBeKept); 

¿Agregar un artículo basado en cada artículo existente? No hay problema:

 myCollection = myCollection.Concat(myCollection.Select(item => new Item(item.SomeProp))); 

No puede cambiar la colección enumerable mientras se enumera, por lo que deberá realizar los cambios antes o después de la enumeración.

El bucle for es una buena alternativa, pero si su colección IEnumerable no implementa ICollection , no es posible.

Ya sea:

1) Copie la colección primero. Enumere la colección copiada y cambie la colección original durante la enumeración. (@tvanfosson)

o

2) Mantenga una lista de cambios y comprométalos después de la enumeración.

El mejor enfoque desde una perspectiva de rendimiento es, probablemente, utilizar una o dos matrices. Copie la lista en una matriz, realice operaciones en la matriz y luego cree una nueva lista a partir de la matriz. Acceder a un elemento de matriz es más rápido que acceder a un elemento de lista, y las conversiones entre una List y una T[] pueden usar una operación rápida de “copia masiva” que evita la sobrecarga asociada al acceso a elementos individuales.

Por ejemplo, supongamos que tiene una List y desea que cada cadena de la lista que comienza con T vaya seguida de un elemento “Boo”, mientras que cada cadena que comience con “U” se descarte por completo. Un enfoque óptimo sería probablemente algo así como:

 int srcPtr,destPtr; string[] arr; srcPtr = theList.Count; arr = new string[srcPtr*2]; theList.CopyTo(arr, theList.Count); // Copy into second half of the array destPtr = 0; for (; srcPtr < arr.Length; srcPtr++) { string st = arr[srcPtr]; char ch = (st ?? "!")[0]; // Get first character of string, or "!" if empty if (ch != 'U') arr[destPtr++] = st; if (ch == 'T') arr[destPtr++] = "Boo"; } if (destPtr > arr.Length/2) // More than half of dest. array is used { theList = new List(arr); // Adds extra elements if (destPtr != arr.Length) theList.RemoveRange(destPtr, arr.Length-destPtr); // Chop to proper length } else { Array.Resize(ref arr, destPtr); theList = new List(arr); // Adds extra elements } 

Hubiera sido útil si List proporcionara un método para construir una lista a partir de una parte de una matriz, pero no conozco ningún método eficiente para hacerlo. Aún así, las operaciones en arreglos son bastante rápidas. Es de destacar el hecho de que agregar y eliminar elementos de la lista no requiere “empujar” alrededor de otros elementos; cada elemento se escribe directamente en su lugar apropiado en la matriz.

Realmente debería usar for() lugar de foreach() en este caso.