Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Casi todos los programas que escriba tendrán que iterar en una colección. Escribirá código que examine todos los elementos de una colección.
También creará métodos de iterador, que son métodos que generan un iterador para los elementos de esa clase. Un iterador es un objeto que atraviesa un contenedor, especialmente las listas. Los iteradores se pueden usar para:
- Realizar una acción en cada elemento de una colección.
- Enumeración de una colección personalizada.
- Extensión de LINQ u otras bibliotecas.
- Creación de una canalización de datos en la que los datos fluyen de forma eficaz a través de métodos de iterador.
El lenguaje C# proporciona características para generar y consumir secuencias. Estas secuencias se pueden producir y consumir de forma sincrónica o asincrónica. En este artículo se proporciona información general sobre esas características.
Iteración con foreach
La enumeración de una colección es sencilla: la foreach
palabra clave enumera una colección y ejecuta la instrucción insertada una vez para cada elemento de la colección:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
Es todo. Para iterar todo el contenido de una colección, la foreach
instrucción es todo lo que necesita. Sin embargo, la foreach
instrucción no es mágica. Se basa en dos interfaces genéricas definidas en la biblioteca de .NET Core para generar el código necesario para iterar una colección: IEnumerable<T>
y IEnumerator<T>
. Este mecanismo se explica con más detalle a continuación.
Ambas interfaces también tienen homólogos no genéricos: IEnumerable
y IEnumerator
. Se prefieren las versiones genéricas para el código moderno.
Cuando se genera una secuencia de forma asincrónica, puede usar la await foreach
instrucción para consumir de forma asincrónica la secuencia:
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}
Cuando una secuencia es , System.Collections.Generic.IEnumerable<T>se usa foreach
. Cuando una secuencia es , System.Collections.Generic.IAsyncEnumerable<T>se usa await foreach
. En este último caso, la secuencia se genera de forma asincrónica.
Orígenes de enumeración con métodos de iterador
Otra gran característica del lenguaje C# permite crear métodos que crean un origen para una enumeración. Estos métodos se conocen como métodos de iterador. Un método de iterador define cómo generar los objetos en una secuencia cuando se solicita. Use las yield return
palabras clave contextuales para definir un método de iterador.
Puede escribir este método para generar la secuencia de enteros 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;
}
El código anterior muestra instrucciones distintas yield return
para resaltar el hecho de que puede usar varias instrucciones discretas yield return
en un método de iterador. Puede (y a menudo hacer) usar otras construcciones de lenguaje para simplificar el código de un método de iterador. La siguiente definición de método genera la misma secuencia exacta de números:
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
int index = 0;
while (index < 10)
yield return index++;
}
No tienes que decidir uno ni otro. Puede tener tantas yield return
instrucciones como sea necesario para satisfacer las necesidades del 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 estos ejemplos anteriores tendrían un homólogo asincrónico. En cada caso, reemplazaría el tipo de valor devuelto de IEnumerable<T>
por .IAsyncEnumerable<T>
Por ejemplo, el ejemplo anterior tendría la siguiente versión asincrónica:
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++;
}
Esa es la sintaxis de los iteradores sincrónicos y asincrónicos. Consideremos un ejemplo real. Imagine que está en un proyecto de IoT y los sensores de dispositivo generan un flujo de datos muy grande. Para obtener una idea de los datos, puede escribir un método que muestree cada elemento de datos Nth. Este pequeño método de iterador hace el truco:
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;
}
}
Si la lectura del dispositivo IoT genera una secuencia asincrónica, modificaría el método como se muestra en el método siguiente:
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;
}
}
Hay una restricción importante en los métodos de iterador: no se puede tener una return
instrucción ni una yield return
instrucción en el mismo método. El código siguiente no se compilará:
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;
}
Esta restricción normalmente no es un problema. Tiene una opción de usar yield return
en todo el método o separar el método original en varios métodos, algunos usan return
y otros usan yield return
.
Puede modificar el último método ligeramente para usarlo yield return
en todas partes:
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;
}
A veces, la respuesta correcta es dividir un método de iterador en dos métodos diferentes. Uno que usa return
y un segundo que usa yield return
. Considere una situación en la que podría querer devolver una colección vacía o los cinco primeros números impares, en función de un argumento booleano. Puede escribirlo como estos dos 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 los métodos anteriores. La primera usa la instrucción estándar return
para devolver una colección vacía o el iterador creado por el segundo método. El segundo método usa la yield return
instrucción para crear la secuencia solicitada.
Profundización en foreach
La foreach
instrucción se expande en un lenguaje estándar que usa las IEnumerable<T>
interfaces y IEnumerator<T>
para iterar en todos los elementos de una colección. También minimiza los errores que los desarrolladores realizan al no administrar correctamente los recursos.
El compilador traduce el foreach
bucle que se muestra en el primer ejemplo en algo similar a esta construcción:
IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
El código exacto generado por el compilador es más complicado y controla situaciones en las que el objeto devuelto por GetEnumerator()
implementa la IDisposable
interfaz. La expansión completa genera código más similar al siguiente:
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
El compilador traduce el primer ejemplo asincrónico en algo similar a esta construcción:
{
var enumerator = collection.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
La manera en que se elimina el enumerador depende de las características del tipo de enumerator
. En el caso sincrónico general, la finally
cláusula se expande a:
finally
{
(enumerator as IDisposable)?.Dispose();
}
El caso asincrónico general se expande a:
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
Sin embargo, si el tipo de enumerator
es un tipo sellado y no hay ninguna conversión implícita del tipo de enumerator
a IDisposable
o IAsyncDisposable
, la finally
cláusula se expande a un bloque vacío:
finally
{
}
Si hay una conversión implícita del tipo de a enumerator
y IDisposable
es un tipo de enumerator
valor que no acepta valores NULL, la finally
cláusula se expande a:
finally
{
((IDisposable)enumerator).Dispose();
}
Afortunadamente, no es necesario recordar todos estos detalles. La foreach
instrucción controla todos esos matices para usted. El compilador generará el código correcto para cualquiera de estas construcciones.