[C#] O poder do IEnumerable e Yield
IEnumerable é uma interface que representa um conjunto de dados que podem ser lidos item por item, podendo apenas percorrer para frente de forma similar a um stream.
Coleções nativas do .NET, como array e List<T>, por padrão já implementam IEnumerable pela facilidade de uso.
Instâncias de IEnumerable costumam ser consumidas por foreach.
IEnumerable<string> items = new string[] { "stub" };
foreach(string item in items)
Console.WriteLine(item.ToLower());
System.Linq disponibiliza alguns extension methods para facilitar o uso do IEnumerable, como por exemplo o Count() para calcular quantos itens existe no conjunto, Select() para gerar um novo objeto para cada item do conjunto e Sum() para fazer a somatória de todos os itens de um conjunto.
using System.Linq;
var items = new int[] { 1, 2, 3 };
var summation = items.Sum();
Por baixo dos panos, um IEnumerable é percorrido através de um IEnumerator gerado por ele, que pode ser consumido manualmente.
IEnumerable<string> items = new string[] { "stub" };
using(IEnumerator<string> enumerator = items.GetEnumerator())
while(enumerator.MoveNext())
Console.WriteLine(enumerator.Current.ToLower());
IEnumerable também pode ser retornado como resultado de uma função geradora, aonde os dados são gerados durante a execução, e não precisam ser carregados todos em memória logo no começo.
No C#, uma função geradora pode ser escrita usando yield.
Funções geradoras são úteis para processar dados sem precisar de um carregamento inicial de todos os items. Isso possibilita a criação de algoritmos com consumo constante de memória - O(1) - enquanto a função é executada.
O exemplo abaixo demonstra uma forma de criar uma função geradora capaz de ler um arquivo .csv de qualquer tamanho por retornar o conteúdo linha por linha.
public static IEnumerable<string> EnumerateLinesOfFile()
{
using (var reader = new StreamReader("file.csv"))
while (!reader.EndOfStream)
yield return reader.ReadLine();
}
static void Main(string[] args)
{
var lineCount = EnumerateLinesOfFile().Count();
var message = $"The file has {lineCount} lines";
Console.WriteLine(message);
}
Outro cenário prático é o processamento do resultado de uma consulta SQL que retorna muitos dados, que podem ser percorridos sem necessidade de carregar todos os dados de uma vez.
Importante notar que a conexão permanece aberta enquanto o loop está executando, e só será fechado quando o IEnumerable sofrer Dispose(), seja implicito (no fim de um foreach) ou explícito (via using ou manualmente).
public IEnumerable<string> EnumerateFromDatabase()
{
using(var connection = new SqlConnection("<connection_string>"))
using(var command = connection.CreateCommand())
{
command.CommandText = "<select>";
connection.Open();
using (var reader = command.ExecuteReader())
while(reader.Read())
yield return reader[0].ToString();
}
}
Funções geradoras só começam sua execução quando o IEnumerable começa a ser percorrido, por exemplo quando um foreach é executado ou através do IEnumerator. Algumas funções Linq, como Count(), FirstOrDefault() e ToList() também executam a função.
Importante notar que algumas funções Linq são funções geradoras e não são imediatamente executadas quando invocadas, por exemplo Select() e Where(). Não há diferença se essas funções forem executados em cima de um IEnumerable de função geracional ou uma coleção carregada em memória.
Outra opção menos comum é a criação de uma função que pode ser infinitamente executada, e que pode ser interrompida por break ou algum método limitador do Linq, como First() ou Take().
/// <summary>Function that generate Fibonacci numbers</summary>
static IEnumerable<int> Fibonacci()
{
int value1 = 1;
int value2 = 1;
yield return value1;
yield return value2;
while (true)
{
int aux = value2;
value2 += value1;
value1 = aux;
yield return value2;
}
}
static void Main(string[] args)
{
// Take the 10th number of fibonacci = 55
var fibonacciTenth = Fibonacci().Skip(9).First();
// Sum the 5 first numbers = 12
var sumFirstFive = Fibonacci().Take(5).Sum();
// Throws exception, because the function evaluation is infinite
var allFibonacciNumbers = Fibonacci().ToList();
}
C# 8.0 também introduziu a interface IAsyncEnumerable para funções geradoras com assincronia. Este link leva a um artigo escrito sobre assincronia.
Para executar métodos Linq em IAsyncEnumerable é necessário adicionar o pacote System.Linq.Async no projeto. Esse pacote é oficialmente suportado pela Microsoft.
// Generator function with async
public async IAsyncEnumerable<string> EnumerateLinesAsync()
{
using(var reader = new StreamReader("path.csv"))
while(!reader.EndOfStream)
yield return await reader.ReadLineAsync();
}
// Consuming an IAsyncEnumerable
await foreach(var line in EnumerateLinesAsync())
{
Console.WriteLine(line);
}
Importante notar que a interface IAsyncEnumerable só está disponível a partir do .NET Standard 2.1 ou .NET Core 3.0, e não está disponível no .NET Framework (atualmente na versão 4.8).
Usar IAsyncEnumerable é a forma recomendada de escrever funções geradoras assíncronas. Não é trivial tentar implementar um IEnumerable com capacidades de assincronia, pois durante a execução o IEnumerator chama o método MoveNext(), que é um método bloqueante e impede os benefícios da assincronia.