Menu

Bucle foreach en C#: arrays, List, Dictionary, índice y errores

Cómo funciona foreach en C#: recorrer arrays, listas, strings y diccionarios (KeyValuePair y deconstrucción), obtener el índice, por qué la variable del bucle es de solo lectura, la InvalidOperationException "Collection was modified" y cómo evitarla, y qué necesita un tipo para usarse en foreach.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

foreach ejecuta su cuerpo una vez por cada elemento de una colección, en orden, sin índice que gestionar:

Salida:

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

El bucle funciona igual con un array, una List<T>, un string (tipo de elemento char), un HashSet<T>, una consulta LINQ o cualquier otra secuencia. El tipo declarado puede ser var, algo habitual cuando el tipo de elemento es largo: foreach (var order in pendingOrders).

Qué hace foreach por debajo

foreach pide a la colección un enumerador y lo recorre. Para una List<int>, el compilador convierte el bucle más o menos en esto:

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

De ahí salen tres consecuencias. Sirve cualquier tipo con un método público GetEnumerator(), que normalmente se obtiene implementando IEnumerable<T>. El enumerador se libera incluso cuando el cuerpo lanza una excepción o hace break. Y la variable del elemento es una copia nueva de e.Current en cada pasada, por eso asignarla no puede cambiar la colección.

Un tipo sin GetEnumerator no se puede usar:

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

Implementa IEnumerable<T> en el tipo, o recorre la colección que contiene (order.Lines en lugar de order). Puedes escribir tus propias secuencias con yield return, lo que se explica en la página de IEnumerable y yield.

Recorrer un Dictionary

Los elementos de un diccionario son valores KeyValuePair<TKey, TValue> con las propiedades Key y Value:

Salida:

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

Un Dictionary no garantiza ningún orden de enumeración. En la práctica sigue el orden de inserción hasta que eliminas entradas, pero el código que depende del orden debería ordenar de forma explícita, como hace el último bucle con OrderByDescending, o usar SortedDictionary.

En .NET Core 2.0 y posteriores (y en todo .NET moderno), KeyValuePair tiene un método Deconstruct, así que el par puede separarse en dos variables directamente en la cabecera del bucle:

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

Es la forma más corta y la que verás en el código actual. No compila en .NET Framework, donde KeyValuePair no tiene Deconstruct; ahí usa item.Key e item.Value.

Obtener el índice

foreach no tiene índice propio. Las dos formas habituales de obtenerlo son un contador que mantienes tú, o la sobrecarga de Select de LINQ que pasa el índice junto con cada elemento:

Salida:

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

El contador es lo más barato y lo más claro. Si te encuentras usando el índice para leer podium[i - 1] o para escribir podium[i] = ..., cambia a un bucle for, que está hecho justo para eso.

La variable del bucle es de solo lectura

No puedes asignar a la variable de iteración:

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

Aunque estuviera permitido, solo cambiaría una copia local. Para transformar cada elemento, construye una secuencia nueva (names.Select(n => n.Trim()).ToArray()) o usa un bucle for que escriba names[i] = names[i].Trim().

Lo que sí puedes hacer depende de si el elemento es una clase o un struct. En una clase, la variable contiene una referencia, así que cambiar una propiedad cambia el objeto de la colección:

Salida:

Order 1: shipped
Order 2: shipped

Si Order fuera un struct, la misma asignación fallaría con CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), porque la variable es una copia del valor, no una referencia a él.

Modificar la colección: InvalidOperationException

Añadir o eliminar elementos de una List o de un Dictionary mientras un foreach la recorre rompe el enumerador. El siguiente MoveNext lanza InvalidOperationException con el mensaje "Collection was modified; enumeration operation may not execute.":

Salida:

InvalidOperationException: collection was modified
milk, bread
milk, bread

Las soluciones, de la más sencilla a la más flexible:

  • RemoveAll(predicate) en una List<T> elimina todas las coincidencias en una pasada.
  • Recorrer una copia con ToList() o ToArray(), y modificar después el original con libertad.
  • Reunir primero, aplicar después: durante el bucle, guarda las claves que hay que borrar en una lista aparte y elimínalas después. Es el patrón habitual con un Dictionary.
  • Un bucle for hacia atrás con RemoveAt(i).

Dictionary tiene una excepción que conviene conocer. Desde .NET Core 3.0, sobrescribir el valor de una clave existente (stock[key] = 0) o eliminar entradas durante un foreach sobre él ya no lanza excepción, mientras que añadir una clave nueva sigue lanzándola. En .NET Framework las tres lanzan, así que reunir primero las claves es la versión que funciona en todas partes.

break, continue y colecciones null

break y continue se comportan como en cualquier otro bucle:

Salida:

reading 21
reading 23
no tags, no crash

Recorrer una colección null lanza NullReferenceException cuando se llama a GetEnumerator. ?? Enumerable.Empty<T>() convierte una secuencia que puede ser null en una vacía; mejor aún, devuelve colecciones vacías en lugar de null desde tus propios métodos.

foreach frente a List.ForEach

List<T> tiene un método ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. No es lo mismo que el bucle. Su cuerpo es una lambda, así que break, continue y yield no están disponibles y return solo termina la llamada del elemento actual. Solo existe en List<T>, no en arrays ni en otras secuencias. Prefiere la sentencia foreach; usa ForEach solo para una acción de una línea que ya tienes como método.

Preguntas frecuentes

¿Cómo recorro un Dictionary con foreach en C#?

Cada elemento es un KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. En .NET Core 2.0 y posteriores puedes deconstruirlo: foreach (var (name, price) in prices). Para recorrer solo las claves o los valores, usa prices.Keys o prices.Values.

¿Cómo obtengo el índice en un bucle foreach de C#?

foreach no tiene un índice incorporado. Mantén un contador (int i = 0; antes del bucle, i++ al final del cuerpo), o proyecta cada elemento con su índice usando LINQ: foreach (var item in names.Select((name, index) => new { name, index })). Si necesitas el índice para escribir de vuelta o comparar vecinos, un bucle for encaja mejor.

¿Qué significa "Collection was modified; enumeration operation may not execute"?

Añadiste o eliminaste elementos de una List, un Dictionary u otra colección mientras un foreach la estaba enumerando. El enumerador detecta el cambio y lanza InvalidOperationException en el siguiente paso. Recorre una copia (foreach (var x in list.ToList())), reúne los cambios y aplícalos después del bucle, usa list.RemoveAll(...) o usa un bucle for hacia atrás.

¿Por qué no puedo asignar a la variable de foreach en C#?

La variable de iteración es de solo lectura: name = name.Trim(); dentro de foreach (string name in names) es el error CS1656. Asignarla tampoco cambiaría la colección. Para sustituir elementos, usa un bucle for con índice, o construye una colección nueva con Select. Cambiar una propiedad de un objeto de clase (order.Status = ...) sí está permitido.

¿Se puede salir de un bucle foreach en C#?

Sí. break termina el bucle de inmediato y continue salta al siguiente elemento, exactamente como en for y while. return sale del método entero. El método List<T>.ForEach es distinto: su cuerpo es una lambda, así que break y continue no están disponibles ahí.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR