csharp-enumerables

Qué son los Enumerables en C#

En C#, el término “enumerable” se refiere a cualquier objeto que implementa la interfaz IEnumerable o IEnumerable<T>. Estas interfaces proporcionan un mecanismo para iterar sobre una colección de elementos.

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 siguiente ModeNext()

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.