Iteradores

Elija un iterador cuando desee devolver una secuencia cuyos elementos llamadores pueden consumir uno a uno, sin crear primero el resultado completo en una colección. Este enfoque se adapta a valores generados, filtros con flujo de control personalizado y secuencias que quienes llaman podrían dejar de leer antes. En comparación con una colección completada, un iterador cambia cuándo se realiza el trabajo, no solo cómo se escribe.

Tip

En este artículo se enseñan iteradores sincrónicos para desarrolladores que conocen métodos, bucles y colecciones. Para obtener una sintaxis completa yield y las restricciones, consulte la referencia de la yield sentencia.

Un método de iterador genera una secuencia de valores de uno en uno. Este artículo se centra en los métodos de iterador que devuelven IEnumerable<T>, cuyos valores el código que llama suele consumir con una instrucción foreach.

Generar elementos con yield return

Use yield return para proporcionar el siguiente elemento en la secuencia. El iterador siguiente selecciona lecturas de temperatura aceptables de un sensor. En este escenario, -1 marca el final de los datos del sensor.

int[] readings = [21, 27, 24, -1, 19];

IEnumerable<int> acceptedReadings =
    SelectTemperatures(readings, maximum: 25);

Console.WriteLine("Sequence created.");

foreach (int temperature in acceptedReadings)
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

static IEnumerable<int> SelectTemperatures(
    IEnumerable<int> readings,
    int maximum)
{
    foreach (int reading in readings)
    {
        Console.WriteLine($"Checking {reading}°C");

        if (reading == -1)
        {
            yield break;
        }

        if (reading <= maximum)
        {
            yield return reading;
        }
    }
}

SelectTemperatures devuelve IEnumerable<int>, que representa una secuencia de int valores. Cada yield return reading proporciona un valor al bucle foreach. Después de que el código llamador controla ese valor, el iterador continúa con la siguiente entrada.

Use el formulario genérico IEnumerable<T> para el código de C# más moderno. El argumento type, como int, indica a los autores de llamada qué tipo de elemento contiene la secuencia.

Finalizar la secuencia con yield break

Utilice yield break para finalizar un iterador antes de que el control llegue al final del método. En el ejemplo, el valor -1 de sentinel significa que no hay más lecturas disponibles:

if (reading == -1)
{
    yield break;
}

El iterador también termina naturalmente cuando la ejecución llega al final de su cuerpo. Use yield break cuando una condición debe detener inmediatamente la producción de elementos.

Consumir un iterador con foreach

Un iterador expone la misma IEnumerable<T> forma que muchas colecciones .NET. Quienes llaman pueden usar foreach sin saber cómo genera la secuencia sus elementos:

foreach (int temperature in SelectTemperatures(readings, 25))
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

El llamador solicita cada elemento a medida que avanza el bucle. El iterador mantiene su posición entre las solicitudes y continúa después de la anterior yield return.

Comprender la producción diferida de elementos

Llamar a un método iterador no ejecuta su cuerpo inmediatamente. El método comienza a generar elementos cuando el código que llama comienza la enumeración. Este comportamiento se denomina ejecución diferida.

El ejemplo imprime Sequence created. antes de cualquier mensaje Checking .... El iterador comienza a comprobar las lecturas solo cuando el bucle foreach solicita su primer elemento.

La ejecución diferida tiene efectos prácticos:

  • El llamador puede comenzar el procesamiento antes de que el iterador genere todos los elementos.
  • El iterador puede detenerse temprano con yield break.
  • El código que llama que nunca enumera la secuencia no ejecuta el cuerpo del iterador.
  • Cada nueva enumeración vuelve a recorrer el iterador.

Deje clara la ejecución diferida en el propósito y la documentación del método. Los lectores no pueden deducir el momento de ejecución solo a partir de IEnumerable<T> porque tanto los iteradores como las colecciones pueden exponer esa interfaz. Mantenga la producción de elementos libre de efectos secundarios no relacionados, ya que quienes llaman pueden enumerar la secuencia más adelante o más de una vez. Los mensajes de diagnóstico del ejemplo muestran esa temporización.

Elegir un iterador o una colección

Devuelve un iterador cuando los valores llegan de forma natural uno a uno o cuando el autor de la llamada no necesita el resultado completo inmediatamente. Devuelve una colección completada cuando el código que llama necesita una instantánea estable, acceso indizado o enumeración repetida sin volver a calcular los elementos.

Ambos enfoques pueden devolver los mismos elementos. El iterador deja la producción del elemento bajo el control del código que llama; un método que construye una colección completa ese trabajo antes de devolver.

Utilice un bucle normal cuando desee realizar una acción para cada elemento sin devolver una secuencia. Para un filtro o transformación estándar, prefiera una operación LINQ como Where o Select; escriba un iterador cuando la lógica personalizada de producción de elementos sea más clara como una serie de instrucciones.

Los métodos de iterador se componen con LINQ porque ambos usan IEnumerable<T>. Por ejemplo, un llamador puede agregar otro filtro:

var comfortableReadings = SelectTemperatures(readings, 25)
    .Where(static temperature => temperature >= 18);

El iterador y la operación LINQ permanecen diferidos hasta que el código enumera comfortableReadings.

Para las secuencias asincrónicas, consulte Generación y consumo de flujos asincrónicos. Para los patrones de enumeración personalizados y todas las yield restricciones, use la referencia de lenguaje.

Consulte también