En C#, los enumerables son un mecanismo para iterar sobre colecciones de elementos. Por ejemplo, es posible emplearlos en bucles FOR EACH o combinarlos entre ellos con LINQ.
Técnicamente, internamente un enumerable
se refiere a cualquier objeto que implementa la interfaz IEnumerable
o IEnumerable<T>
.
Vamos a verlo en detalle 👇.
Interfaz IEnumerable e IEnumerator
La interfaz IEnumerable
define un único método GetEnumerator()
, que devuelve un IEnumerator
. El enumerador proporciona la funcionalidad necesaria para iterar a través de una colección.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
Exista también la versión genérica IEnumerable<T>
, que hereda de IEnumerable
.
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
La interfaz IEnumerable
está definida en el espacio de nombres System.Collections
y su versión genérica IEnumerable<T>
en System.Collections.Generic
.
Interface IEnumerator
Por su parte, el interfaz IEnumerator
y su equivalente genérico IEnumerator<T>
tienen la siguiente pinta.
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
public interface IEnumerator<out T> : IEnumerator
{
T Current { get; }
}
Es decir, básicamente es un elemento que,
- Permite iterar sobre una serie de elementos
- Contiene una referencia al elemento actual
Current
y al siguienteModeNext()
Cómo usar los enumerables
La forma más común de iterar sobre un enumerable
es mediante el uso de foreach
. Este bucle simplifica la sintaxis de iteración al ocultar los detalles del enumerador.
List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };
foreach (int numero in numeros)
{
Console.WriteLine(numero);
}
Implementación de un enumerable personalizado
Podemos crear una clase que implemente IEnumerable<T>
para definir una colección personalizada.
Aquí tenemos un ejemplo de un Enumerable que genera una secuencia de números pares.
public class Pares : IEnumerable<int>
{
private int _max;
public Pares(int max)
{
_max = max;
}
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i <= _max; i += 2)
{
yield return i;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Uso de yield return
La palabra clave yield return
simplifica la creación de enumeradores personalizados. Podemos usarla para devolver elementos sobre la marcha.
public static IEnumerable<int> GenerarPares(int max)
{
for (int i = 0; i <= max; i += 2)
{
yield return i;
}
}
En este ejemplo, el método ObtenerNumerosPares utiliza yield return para devolver los números pares hasta un máximo especificado.
Uso de LINQ con IEnumerable
LINQ (Language Integrated Query) es que nos permite realizar consultas sobre colecciones de manera declarativa. Las consultas LINQ funcionan con cualquier colección que implemente IEnumerable<T>
.
List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var numerosPares = from numero in numeros
where numero % 2 == 0
select numero;
foreach (int numero in numerosPares)
{
Console.WriteLine(numero);
}
Evitar modificar colecciones durante la iteración
Modificar una colección mientras se itera sobre ella puede causar excepciones o comportamientos inesperados. Lo más probable es que acabe en 💥.
En su lugar, debemos crear una nueva colección con los elementos modificados.
List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };
List<int> numerosDuplicados = new List<int>();
foreach (int numero in numeros)
{
numerosDuplicados.Add(numero * 2);
}
Comprender la evaluación diferida
Las consultas LINQ y los métodos que utilizan yield return
se evalúan de manera diferida. Es decir, no se ejecutan hasta que se itera sobre la colección.
Esto es una característica muy interesante, pero también es una fuente de errores si no lo entendemos correctamente.
List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };
var filtrado = numeros.Where(n => n > 3);
Console.WriteLine(filtrado.Sum()); // 9
numeros.Add(6);
Console.WriteLine(filtrado.Sum()); // 15
En el ejemplo,
- Creamos una colección de números de 1 a 5,
- Usamos
Where
para filtrar los números mayores que 3 - Mostramos la suma, que es 9 (4 + 5)
- Añadimos un 6
- Mostramos la suma que ahora es 16
Esto es así porque Where
es un método que trabajo con enumerables. Por tanto, filtrado
no es una colección de números, si no un iterable que depende de numeros
. Al añadir a numeros
el numero 6, filtrados se modifica, y por tanto su suma.