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

Demo en vivo en .NET Fiddle

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

Demo en vivo en .NET Fiddle

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

Demo en vivo en .NET Fiddle

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

Ver demostración

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 a IEnumerator.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 un while 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

Ver demostración

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

Ver demostración

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):

  1. El compilador C # genera una clase que implementa IEnumerable<BigInteger> y IEnumerator<BigInteger> ( <Fibonacci>d__0 en ildasm).
  2. 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.
  3. Los códigos más interesantes están en el bool IEnumerator.MoveNext() . Básicamente, lo que MoveNext() hace:
    • Restaura el estado actual. Variables como prev y current 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 en yield return .
    • Ejecuta el código hasta la próxima yield return o la yield break / } .
    • Para el yield return valor resultante se guarda, por lo que la propiedad Current puede devolverlo. se devuelve el true En este punto, el estado actual se guarda de nuevo para la siguiente invocación de MoveNext .
    • Para el método de yield break / } , solo se devuelve el significado false se realizó la iteración.

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;
    }


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow