Iteradores

Quase todos os programas que você escreve terão alguma necessidade de iterar sobre uma coleção. Você escreverá um código que examina cada item de uma coleção.

Você também criará métodos iteradores, que são métodos que produzem um iterador para os elementos dessa classe. Um iterador é um objeto que atravessa um contêiner, particularmente listas. Os iteradores podem ser usados para:

  • Executar uma ação em cada item de uma coleção.
  • Enumerando uma coleção personalizada.
  • Extensão do LINQ ou de outras bibliotecas.
  • Criação de um pipeline de dados onde os dados fluem de forma eficiente através de métodos iteradores.

A linguagem C# fornece recursos para gerar e consumir sequências. Essas sequências podem ser produzidas e consumidas de forma síncrona ou assíncrona. Este artigo fornece uma visão geral desses recursos.

Iteração com foreach

Enumerar uma coleção é simples: A foreach palavra-chave enumera uma coleção, executando a instrução embedded uma vez para cada elemento da coleção:

foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

É tudo. Para iterar todo o conteúdo de uma coleção, a declaração é tudo o foreach que você precisa. A foreach afirmação não é mágica, porém. Ele depende de duas interfaces genéricas definidas na biblioteca principal do .NET para gerar o código necessário para iterar uma coleção: IEnumerable<T> e IEnumerator<T>. Este mecanismo é explicado mais pormenorizadamente a seguir.

Ambas as interfaces também têm contrapartes não genéricas: IEnumerable e IEnumerator. As versões genéricas são preferidas para o código moderno.

Quando uma sequência é gerada de forma assíncrona, você pode usar a await foreach instrução para consumir a sequência de forma assíncrona:

await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}

Quando uma sequência é um System.Collections.Generic.IEnumerable<T>, você usa foreach. Quando uma sequência é um System.Collections.Generic.IAsyncEnumerable<T>, você usa await foreach. Neste último caso, a sequência é gerada de forma assíncrona.

Fontes de enumeração com métodos iteradores

Outro grande recurso da linguagem C# permite que você crie métodos que criam uma fonte para uma enumeração. Esses métodos são chamados de métodos iteradores. Um método iterador define como gerar os objetos em uma sequência quando solicitado. Você usa as palavras-chave contextuais yield return para definir um método iterador.

Você pode escrever este método para produzir a sequência de inteiros de 0 a 9:

public IEnumerable<int> GetSingleDigitNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
    yield return 6;
    yield return 7;
    yield return 8;
    yield return 9;
}

O código acima mostra instruções distintas yield return para destacar o fato de que você pode usar várias instruções discretas yield return em um método iterador. Você pode (e muitas vezes faz) usar outras construções de linguagem para simplificar o código de um método iterador. A definição do método abaixo produz exatamente a mesma sequência de números:

public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

Você não precisa decidir um ou outro. Você pode ter quantas yield return declarações forem necessárias para atender às necessidades do seu método:

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

Todos esses exemplos anteriores teriam uma contrapartida assíncrona. Em cada caso, você substituiria o tipo de retorno de IEnumerable<T> por um IAsyncEnumerable<T>arquivo . Por exemplo, o exemplo anterior teria a seguinte versão assíncrona:

public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

Essa é a sintaxe para iteradores síncronos e assíncronos. Vamos considerar um exemplo do mundo real. Imagine que você está em um projeto de IoT e os sensores do dispositivo geram um fluxo muito grande de dados. Para ter uma ideia dos dados, você pode escrever um método que faça uma amostra de cada elemento de dados Nth. Este pequeno método iterador faz o truque:

public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

Se a leitura do dispositivo IoT produzir uma sequência assíncrona, você modificará o método como mostra o método a seguir:

public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    await foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

Há uma restrição importante nos métodos iteradores: você não pode ter uma return instrução e uma yield return instrução no mesmo método. O código a seguir não será compilado:

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    // generates a compile time error:
    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    return items;
}

Essa restrição normalmente não é um problema. Você tem a opção de usar yield return todo o método ou separar o método original em vários métodos, alguns usando return, e outros usando yield return.

Você pode modificar ligeiramente o último método para usar yield return em todos os lugares:

public IEnumerable<int> GetFirstDecile()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    foreach (var item in items)
        yield return item;
}

Às vezes, a resposta certa é dividir um método iterador em dois métodos diferentes. Um que usa return, e um segundo que usa yield return. Considere uma situação em que você pode querer retornar uma coleção vazia, ou os primeiros cinco números ímpares, com base em um argumento booleano. Você pode escrever isso como estes dois métodos:

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

Veja os métodos acima. O primeiro usa a instrução standard return para retornar uma coleção vazia ou o iterador criado pelo segundo método. O segundo método usa a yield return instrução para criar a sequência solicitada.

Aprofunde-se foreach

A foreach instrução se expande para uma linguagem padrão que usa as IEnumerable<T> interfaces e IEnumerator<T> para iterar em todos os elementos de uma coleção. Ele também minimiza os erros que os desenvolvedores cometem ao não gerenciar recursos corretamente.

O compilador traduz o foreach loop mostrado no primeiro exemplo em algo semelhante a esta construção:

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

O código exato gerado pelo compilador é mais complicado e lida com situações em que o objeto retornado por GetEnumerator() implementa a IDisposable interface. A expansão completa gera código mais parecido com este:

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of enumerator.
    }
}

O compilador traduz o primeiro exemplo assíncrono em algo semelhante a esta construção:

{
    var enumerator = collection.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of async enumerator.
    }
}

A maneira como o recenseador é eliminado depende das características do tipo de enumerator. No caso geral síncrono, a finally cláusula expande-se para:

finally
{
   (enumerator as IDisposable)?.Dispose();
}

O caso assíncrono geral expande-se para:

finally
{
    if (enumerator is IAsyncDisposable asyncDisposable)
        await asyncDisposable.DisposeAsync();
}

No entanto, se o tipo de enumerator for um tipo selado e não houver conversão implícita do tipo de enumerator para IDisposable ou IAsyncDisposable, a finally cláusula se expande para um bloco vazio:

finally
{
}

Se houver uma conversão implícita do tipo de para IDisposable, e enumerator for um tipo de enumerator valor não anulável, a finally cláusula se expandirá para:

finally
{
   ((IDisposable)enumerator).Dispose();
}

Felizmente, você não precisa se lembrar de todos esses detalhes. A foreach declaração lida com todas essas nuances para você. O compilador irá gerar o código correto para qualquer uma dessas construções.