C# Language
Palabra clave de rendimiento
Buscar..
Introducción
Cuando utiliza la palabra clave de rendimiento en una declaración, indica que el método, el operador o el elemento de acceso en el que aparece es un iterador. El uso del rendimiento para definir un iterador elimina la necesidad de una clase adicional explícita (la clase que mantiene el estado para una enumeración) cuando implementa el patrón IEnumerable e IEnumerator para un tipo de colección personalizado.
Sintaxis
- rendimiento [TIPO]
- pausa de rendimiento
Observaciones
Poner la palabra clave de yield
en un método con el tipo de retorno de IEnumerable
, IEnumerable<T>
, IEnumerator
o IEnumerator<T>
le dice al compilador que genere una implementación del tipo de retorno ( IEnumerable
o IEnumerator
) que, cuando se realiza un bucle, ejecuta el Método hasta cada "rendimiento" para obtener cada resultado.
La palabra clave de yield
es útil cuando desea devolver "el siguiente" elemento de una secuencia teóricamente ilimitada, por lo que sería imposible calcular la secuencia completa de antemano, o cuando calcular la secuencia completa de valores antes de regresar daría lugar a una pausa indeseable para el usuario .
yield break
también se puede utilizar para terminar la secuencia en cualquier momento.
Como la palabra clave de yield
requiere un tipo de interfaz de iterador como tipo de retorno, como IEnumerable<T>
, no puede usar esto en un método asíncrono ya que devuelve un objeto Task<IEnumerable<T>>
.
Otras lecturas
Uso simple
La palabra clave de yield
se utiliza para definir una función que devuelve un IEnumerable
o IEnumerator
(así como sus variantes genéricas derivadas) cuyos valores se generan perezosamente cuando un llamante recorre la colección devuelta. Lea más sobre el propósito en la sección de comentarios .
El siguiente ejemplo tiene una declaración de rendimiento que está dentro de un bucle for
.
public static IEnumerable<int> Count(int start, int count)
{
for (int i = 0; i <= count; i++)
{
yield return start + i;
}
}
Entonces puedes llamarlo:
foreach (int value in Count(start: 4, count: 10))
{
Console.WriteLine(value);
}
Salida de consola
4
5
6
...
14
Cada iteración del cuerpo de la instrucción foreach
crea una llamada a la función de iterador de Count
. Cada llamada a la función de iterador pasa a la siguiente ejecución de la declaración de yield return
, que se produce durante la siguiente iteración del bucle for
.
Uso más pertinente
public IEnumerable<User> SelectUsers()
{
// Execute an SQL query on a database.
using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
{
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
yield return new User(id, name);
}
}
}
Por supuesto, hay otras formas de obtener un IEnumerable<User>
de una base de datos SQL. Esto solo demuestra que puede usar el yield
para convertir cualquier cosa que tenga la semántica de "secuencia de elementos" en un IEnumerable<T>
que alguien puede iterar. .
Terminación anticipada
Puede ampliar la funcionalidad de los métodos de yield
existentes pasando uno o más valores o elementos que podrían definir una condición de terminación dentro de la función llamando a un yield break
para detener la ejecución del ciclo interno.
public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
int curr = start;
while (true)
{
if (earlyTerminationSet.Contains(curr))
{
// we've hit one of the ending values
yield break;
}
yield return curr;
if (curr == Int32.MaxValue)
{
// don't overflow if we get all the way to the end; just stop
yield break;
}
curr++;
}
}
El método anterior se repetirá desde una posición de start
dada hasta que se encuentre uno de los valores dentro del conjunto de earlyTerminationSet
.
// Iterate from a starting point until you encounter any elements defined as
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
// This will write out the results from 1 until 7 (which will trigger terminating)
Console.WriteLine(x);
}
Salida:
1
2
3
4
5
6
Comprobando correctamente los argumentos
Un método iterador no se ejecuta hasta que se enumera el valor de retorno. Por lo tanto, es ventajoso afirmar condiciones previas fuera del iterador.
public static IEnumerable<int> Count(int start, int count)
{
// The exception will throw when the method is called, not when the result is iterated
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
return CountCore(start, count);
}
private static IEnumerable<int> CountCore(int start, int count)
{
// If the exception was thrown here it would be raised during the first MoveNext()
// call on the IEnumerator, potentially at a point in the code far away from where
// an incorrect value was passed.
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
Código lateral de llamada (uso):
// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
Console.WriteLine(x);
}
Salida:
1
2
3
4
5
6
7
8
9
10
Cuando un método utiliza el yield
para generar un enumerable, el compilador crea una máquina de estado que, cuando se itera, ejecuta el código hasta un yield
. A continuación, devuelve el elemento cedido y guarda su estado.
Esto significa que no encontrará información sobre argumentos no válidos (pasar null
etc.) cuando llame por primera vez al método (porque eso crea la máquina de estado), solo cuando intente acceder al primer elemento (porque solo entonces el código dentro de la método se ejecuta por la máquina de estado). Al ajustarlo en un método normal que primero verifica los argumentos, puede verificarlos cuando se llama al método. Este es un ejemplo de fallar rápido.
Cuando se usa C # 7+, la función CountCore
se puede ocultar convenientemente en la función Count
como una función local . Vea el ejemplo aquí .
Devuelve otro Enumerable dentro de un método que devuelve Enumerable.
public IEnumerable<int> F1()
{
for (int i = 0; i < 3; i++)
yield return i;
//return F2(); // Compile Error!!
foreach (var element in F2())
yield return element;
}
public int[] F2()
{
return new[] { 3, 4, 5 };
}
Evaluación perezosa
Solo cuando la instrucción foreach
mueve al siguiente elemento, el bloque iterador evalúa hasta la siguiente declaración de yield
.
Considere el siguiente ejemplo:
private IEnumerable<int> Integers()
{
var i = 0;
while(true)
{
Console.WriteLine("Inside iterator: " + i);
yield return i;
i++;
}
}
private void PrintNumbers()
{
var numbers = Integers().Take(3);
Console.WriteLine("Starting iteration");
foreach(var number in numbers)
{
Console.WriteLine("Inside foreach: " + number);
}
}
Esto dará como resultado:
Iteración inicial
Iterador interior: 0
Dentro de foreach: 0
Iterador interior: 1
Dentro de foreach: 1
Iterador interior: 2
Dentro de foreach: 2
Como consecuencia:
- La "iteración de inicio" se imprime primero aunque se llamó al método del iterador antes de que la línea lo imprimiera porque la línea
Integers().Take(3);
en realidad no inicia la iteración (no se realizó ninguna llamada aIEnumerator.MoveNext()
) - Las líneas que se imprimen en la consola alternan entre la que está dentro del método del iterador y la que está dentro del
foreach
, en lugar de todas las que están dentro del método del iterador evaluando primero - Este programa termina debido al método
.Take()
, aunque el método del iterador tiene unwhile true
que nunca se rompe.
Intenta ... finalmente
Si un método de iterador tiene un rendimiento dentro de un try...finally
, el IEnumerator
devuelto ejecutará la instrucción finally
cuando se llame a Dispose
, siempre que el punto actual de evaluación esté dentro del bloque try
.
Dada la función:
private IEnumerable<int> Numbers()
{
yield return 1;
try
{
yield return 2;
yield return 3;
}
finally
{
Console.WriteLine("Finally executed");
}
}
Al llamar:
private void DisposeOutsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Luego se imprime:
1
Al llamar:
private void DisposeInsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Luego se imprime:
1
2
Finalmente ejecutado
Usando el rendimiento para crear un IEnumerator al implementar IEnumerable
La IEnumerable<T>
tiene un método único, GetEnumerator()
, que devuelve un IEnumerator<T>
.
Si bien la palabra clave de yield
se puede usar para crear directamente un IEnumerable<T>
, también se puede usar exactamente de la misma manera para crear un IEnumerator<T>
. Lo único que cambia es el tipo de retorno del método.
Esto puede ser útil si queremos crear nuestra propia clase que implementa IEnumerable<T>
:
public class PrintingEnumerable<T> : IEnumerable<T>
{
private IEnumerable<T> _wrapped;
public PrintingEnumerable(IEnumerable<T> wrapped)
{
_wrapped = wrapped;
}
// This method returns an IEnumerator<T>, rather than an IEnumerable<T>
// But the yield syntax and usage is identical.
public IEnumerator<T> GetEnumerator()
{
foreach(var item in _wrapped)
{
Console.WriteLine("Yielding: " + item);
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
(Tenga en cuenta que este ejemplo en particular es solo ilustrativo y podría implementarse de forma más limpia con un solo método iterador que devuelva un IEnumerable<T>
).
Evaluación impaciente
La palabra clave de yield
permite la evaluación perezosa de la colección. La carga forzada de toda la colección en la memoria se llama evaluación impaciente .
El siguiente código muestra esto:
IEnumerable<int> myMethod()
{
for(int i=0; i <= 8675309; i++)
{
yield return i;
}
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();
Llamar a ToList
, ToDictionary
o ToArray
forzará la evaluación inmediata de la enumeración, recuperando todos los elementos en una colección.
Ejemplo de evaluación perezosa: números de Fibonacci
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics
namespace ConsoleApplication33
{
class Program
{
private static IEnumerable<BigInteger> Fibonacci()
{
BigInteger prev = 0;
BigInteger current = 1;
while (true)
{
yield return current;
var next = prev + current;
prev = current;
current = next;
}
}
static void Main()
{
// print Fibonacci numbers from 10001 to 10010
var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
Console.WriteLine(string.Join(Environment.NewLine, numbers));
}
}
}
Cómo funciona bajo el capó (recomiendo descompilar el archivo .exe resultante en la herramienta IL Disaambler):
- El compilador C # genera una clase que implementa
IEnumerable<BigInteger>
yIEnumerator<BigInteger>
(<Fibonacci>d__0
en ildasm). - Esta clase implementa una máquina de estado. El estado consiste en la posición actual en el método y los valores de las variables locales.
- Los códigos más interesantes están en el
bool IEnumerator.MoveNext()
. Básicamente, lo queMoveNext()
hace:- Restaura el estado actual. Variables como
prev
ycurrent
convierten en campos en nuestra clase (<current>5__2
y<prev>5__1
en ildasm). En nuestro método tenemos dos posiciones (<>1__state
): primero en la llave de apertura, segundo enyield return
. - Ejecuta el código hasta la próxima
yield return
o layield break
/}
. - Para el
yield return
valor resultante se guarda, por lo que la propiedadCurrent
puede devolverlo. se devuelve eltrue
En este punto, el estado actual se guarda de nuevo para la siguiente invocación deMoveNext
. - Para el método de
yield break
/}
, solo se devuelve el significadofalse
se realizó la iteración.
- Restaura el estado actual. Variables como
También tenga en cuenta que ese número 10001 tiene 468 bytes de longitud. La máquina de estado solo guarda current
variables current
y prev
como campos. Si bien, si nos gustaría guardar todos los números en la secuencia desde el primero hasta el 10000, el tamaño de memoria consumido será de más de 4 megabytes. Por lo tanto, la evaluación perezosa, si se usa correctamente, puede reducir la huella de memoria en algunos casos.
La diferencia entre rotura y rotura de rendimiento.
Usar la yield break
en lugar de la break
puede no ser tan obvio como se podría pensar. Hay muchos ejemplos malos en Internet donde el uso de los dos es intercambiable y no demuestra la diferencia.
La parte confusa es que ambas palabras clave (o frases clave) solo tienen sentido dentro de los bucles ( foreach
, while
...) Entonces, ¿cuándo elegir una sobre la otra?
Es importante darse cuenta de que una vez que use la palabra clave de yield
en un método, convertirá el método en un iterador . El único propósito de tal método es, entonces, iterar sobre una colección finita o infinita y producir (salida) sus elementos. Una vez que se cumple el propósito, no hay razón para continuar con la ejecución del método. A veces, sucede naturalmente con el último corchete de cierre del método }
. Pero a veces, quieres terminar el método prematuramente. En un método normal (no iterativo), usaría la palabra clave return
. Pero no se puede usar el return
en un iterador, se debe usar la yield break
. En otras palabras, el yield break
para un iterador es el mismo que el return
para un método estándar. Mientras que, la instrucción break
solo termina el bucle más cercano.
Veamos algunos ejemplos:
/// <summary>
/// Yields numbers from 0 to 9
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
public static IEnumerable<int> YieldBreak()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Indicates that the iteration has ended, everything
// from this line on will be ignored
yield break;
}
}
yield return 10; // This will never get executed
}
/// <summary>
/// Yields numbers from 0 to 10
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
public static IEnumerable<int> Break()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Terminates just the loop
break;
}
}
// Execution continues
yield return 10;
}