Iteradores

Escolha um iterador quando quiser devolver uma sequência cujos chamadores de elementos possam consumir um de cada vez, sem antes construir o resultado completo numa coleção. Esta abordagem ajusta valores gerados, filtra com fluxo de controlo personalizado e sequências que os chamadores podem deixar de ler mais cedo. Comparado com uma coleção concluída, um iterador muda quando o trabalho acontece, não apenas a forma como o escreves.

Tip

Este artigo ensina iteradores síncronos para programadores que conhecem métodos, ciclos e coleções. Para sintaxe completa yield e restrições, consulte a yield referência da afirmação.

Um método iterador produz uma sequência de valores um de cada vez. Este artigo foca-se em métodos iteradores que retornam IEnumerable<T>, cujos valores os chamadores normalmente consomem com uma foreach instrução.

Produzir elementos com yield return

Use yield return para fornecer o próximo elemento na sequência. O iterador seguinte seleciona leituras de temperatura aceitáveis a partir de um sensor. Neste cenário, -1 marca o fim dos dados do 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 devolve IEnumerable<int>, que representa uma sequência de int valores. Cada um yield return reading fornece um valor ao foreach ciclo. Depois de o chamador tratar esse valor, o iterador continua com a próxima entrada.

Use a forma genérica IEnumerable<T> para a maioria dos códigos C# modernos. O argumento do tipo, como int, indica aos chamadores que tipo de elemento a sequência contém.

Termina a sequência com yield break

Use yield break para terminar um iterador antes de o controlo chegar ao fim do método. No exemplo, o valor -1 sentinela significa que não existem mais leituras disponíveis:

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

O iterador também termina naturalmente quando a execução chega ao fim do seu corpo. Use yield break quando uma condição deve parar imediatamente a produção de elementos.

Consumir um iterador com foreach

Um iterador expõe a mesma IEnumerable<T> forma que muitas coleções .NET. Os que chamam podem usar foreach sem saber como a sequência produz os seus elementos:

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

O chamador solicita cada elemento à medida que o ciclo avança. O iterador mantém a sua posição entre pedidos e continua após o anterior yield return.

Compreender a produção diferida de elementos

Chamar um método iterador não executa imediatamente o seu corpo. O método começa a produzir elementos quando o chamador inicia a enumeração. Este comportamento chama-se execução diferida.

O exemplo imprime Sequence created. antes de qualquer Checking ... mensagem. O iterador começa a verificar as leituras apenas quando o foreach ciclo pede o seu primeiro elemento.

A execução diferida tem efeitos práticos:

  • O chamador pode começar o processamento antes de o iterador produzir todos os elementos.
  • O iterador pode parar cedo com yield break.
  • Um chamador que nunca enumera a sequência não executa o corpo do iterador.
  • Cada nova enumeração executa novamente o iterador.

Deixe clara a execução diferida no propósito e documentação do método. Os leitores não conseguem inferir o tempo de execução sozinhos IEnumerable<T> porque tanto iteradores como coleções conseguem expor essa interface. Mantenha a produção de elementos livre de efeitos secundários não relacionados porque os chamadores podem enumerar a sequência mais tarde ou mais do que uma vez. As mensagens de diagnóstico do exemplo ilustram esse timing.

Escolha um iterador ou coleção

Devolva um iterador quando os valores chegam naturalmente um de cada vez ou quando o chamador não precisa imediatamente do resultado completo. Devolve uma coleção concluída quando os chamadores precisam de um instantâneo estável, acesso indexado ou enumeração repetida sem recalcular os elementos.

Ambas as abordagens podem devolver os mesmos elementos. O iterador deixa a produção de elementos sob controlo do chamador; um método que constrói uma coleção conclui esse trabalho antes de ele regressar.

Usa um loop comum quando quiseres realizar uma ação para cada item sem devolver uma sequência. Para um filtro ou transformação padrão, prefira uma operação LINQ como Where ou Select; escreva um iterador quando a lógica de produção de elementos personalizada for mais clara como uma série de instruções.

Os métodos iteradores compõem com LINQ porque ambos usam IEnumerable<T>. Por exemplo, um chamador pode adicionar outro filtro:

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

O iterador e a operação LINQ permanecem adiados até que o código enumere comfortableReadings.

Para sequências assíncronas, veja Gerar e consumir fluxos assíncronos. Para padrões de enumeração personalizados e todas as yield restrições, use a referência linguística.

Ver também