Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Quase todos os programas que você escrever terão alguma necessidade de iterar em uma coleção. Você escreverá um código que examina todos os itens de uma coleção.
Você também criará métodos de iterador, 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:
- Executando uma ação sobre cada item em uma coleção.
- Enumerar uma coleção personalizada.
- Estender LINQ ou outras bibliotecas.
- Criando um pipeline de dados onde os dados fluem com eficiência através de métodos iterativos.
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.
Iterando usando foreach
Enumerar uma coleção é simples: a foreach
palavra-chave enumera uma coleção, executando a instrução inserida uma vez para cada elemento na coleção:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
Isso é tudo. Para iterar em todo o conteúdo de uma coleção, a instrução foreach
é tudo o que você precisa. A foreach
declaração não é mágica, no entanto. Ele depende de duas interfaces genéricas definidas na biblioteca do .NET Core para gerar o código necessário para iterar uma coleção: IEnumerable<T>
e IEnumerator<T>
. Esse mecanismo é explicado com mais detalhes abaixo.
Ambas as interfaces também têm equivalentes não genéricos: IEnumerable
e IEnumerator
. As versões genéricas são preferenciais para 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 é uma System.Collections.Generic.IEnumerable<T>, você usa foreach
. Quando uma sequência é uma System.Collections.Generic.IAsyncEnumerable<T>, você usa await foreach
. No último caso, a sequência é gerada de forma assíncrona.
Fontes de enumeração com métodos de iteração
Outro ótimo recurso da linguagem C# permite criar métodos que criam uma origem para uma enumeração. Esses métodos são chamados de métodos de iterador. Um método iterador define como gerar os objetos em uma sequência quando solicitado. Você usa as yield return
palavras-chave contextuais 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 realçar o fato de que você pode usar várias instruções discretas yield return
em um método iterador. Você pode (e muitas vezes usar) outros constructos 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 tem que decidir um ou outro. Você pode ter quantas instruções yield return
forem necessárias para atender as 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 um equivalente assíncrono. Em cada caso, você substituiria o tipo de retorno de IEnumerable<T>
por um IAsyncEnumerable<T>
. 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 de dispositivo geram um fluxo muito grande de dados. Para obter uma ideia dos dados, você pode escrever um método que amostra cada elemento de dados Nth. Esse pequeno método iterador resolve:
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 seguinte método:
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 em métodos de iterador: você não pode ter uma instrução return
e uma instrução yield return
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 o último método ligeiramente 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 talvez você queira retornar uma coleção vazia ou os cinco primeiros números ímpares, com base em um argumento booliano. 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++;
}
}
Examine os métodos acima. O primeiro usa a instrução padrão 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.
Aprofundamento em foreach
A instrução foreach
se expande em uma expressão padrão que usa as interfaces IEnumerable<T>
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 corretamente os recursos.
O compilador converte o foreach
loop mostrado no primeiro exemplo em algo semelhante a este constructo:
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 GetEnumerator()
implementa a IDisposable
interface. A expansão completa gera um 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 converte o primeiro exemplo assíncrono em algo semelhante a este constructo:
{
var enumerator = collection.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
A maneira pela qual o enumerador é descartado depende das características do tipo de enumerator
. No caso síncrono geral, a finally
cláusula se expande para:
finally
{
(enumerator as IDisposable)?.Dispose();
}
O caso assíncrono geral se expande para:
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
No entanto, se o tipo de enumerator
for um tipo lacrado e não houver nenhuma conversão implícita do tipo de enumerator
para IDisposable
ou IAsyncDisposable
, a cláusula finally
se expande para um bloco vazio.
finally
{
}
Se houver uma conversão implícita do tipo enumerator
para IDisposable
, e enumerator
for um tipo de valor não anulável, a cláusula finally
se expande para:
finally
{
((IDisposable)enumerator).Dispose();
}
Felizmente, você não precisa se lembrar de todos esses detalhes. A instrução foreach
trata todas essas nuances para você. O compilador gerará o código correto para qualquer uma dessas construções.