Pregunta ¿Cuál es la palabra clave yield utilizada en C #?


En el ¿Cómo puedo exponer solo un fragmento de IList <> pregunta una de las respuestas tenía el siguiente fragmento de código:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item )
            yield return item;
    }
}

¿Qué significa la palabra clave yield aquí? Lo he visto referenciado en un par de lugares, y otra pregunta, pero no he descifrado qué es lo que realmente hace. Estoy acostumbrado a pensar en el rendimiento en el sentido de un hilo cediendo a otro, pero eso no parece relevante aquí.


657
2017-09-02 13:15


origen


Respuestas:


La palabra clave de rendimiento realmente hace bastante aquí. La función devuelve un objeto que implementa la interfaz IEnumerable. Si una función de llamada comienza a extenderse sobre este objeto, la función se llama nuevamente hasta que "cede". Este es el azúcar sintáctico introducido en C # 2.0. En versiones anteriores, tenía que crear sus propios objetos IEnumerable e IEnumerator para hacer cosas como esta.

La forma más fácil de entender un código como este es escribir un ejemplo, establecer algunos puntos de interrupción y ver qué sucede.

Intente dar un paso a través de esto, por ejemplo:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Cuando revise el ejemplo, encontrará que la primera llamada a Enteros () devuelve 1. La segunda llamada devuelve 2 y la línea "rendimiento devuelto 1" no se ejecuta nuevamente.

Aquí hay un ejemplo de la vida real

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

580
2017-09-02 13:23



Iteración. Crea una máquina de estado "debajo de las sábanas" que recuerda dónde estaba en cada ciclo adicional de la función y recoge desde allí.


323
2017-09-02 13:17



El rendimiento tiene dos grandes usos,

  1. Ayuda a proporcionar iteraciones personalizadas sin crear colecciones temporales.

  2. Ayuda a hacer una iteración con estado. enter image description here

Para explicar los dos puntos anteriores de manera más demostrativa, he creado un video simple que puedes ver aquí


145
2018-04-12 17:31



Recientemente, Raymond Chen también publicó una interesante serie de artículos sobre la palabra clave yield.

Si bien se usa nominalmente para implementar fácilmente un patrón de iterador, pero se puede generalizar en una máquina de estado. No tiene sentido citar a Raymond, la última parte también se vincula con otros usos (pero el ejemplo en el blog de Entin es especialmente bueno, y muestra cómo escribir un código seguro asincrónico).


124
2017-09-02 13:27



A primera vista, el retorno de rendimiento es un azúcar .NET para devolver un IEnumerable.

Sin rendimiento, todos los elementos de la colección se crean a la vez:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Mismo código usando rendimiento, devuelve artículo por artículo:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

La ventaja de usar el rendimiento es que si la función que consume sus datos simplemente necesita el primer elemento de la colección, el resto de los elementos no se crearán.

El operador de rendimiento permite la creación de artículos según se lo exija. Esa es una buena razón para usarlo.


46
2018-01-17 14:23



yield return se usa con enumeradores. En cada llamada de declaración de rendimiento, el control se devuelve a la persona que llama, pero asegura que se mantenga el estado del destinatario. Debido a esto, cuando la persona que llama enumera el siguiente elemento, continúa la ejecución en el método callee de la instrucción inmediatamente después del yielddeclaración.

Intentemos entender esto con un ejemplo. En este ejemplo, correspondiente a cada línea, he mencionado el orden en que fluye la ejecución.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Además, el estado se mantiene para cada enumeración. Supongamos que tengo otra llamada a Fibs() método entonces el estado se restablecerá para ello.


29
2018-02-25 17:15



Intuitivamente, la palabra clave devuelve un valor de la función sin abandonarla, es decir, en el ejemplo del código, devuelve el valor actual. item valor y luego reanuda el bucle. Más formalmente, es utilizado por el compilador para generar código para un iterador. Iteradores son funciones que regresan IEnumerable objetos. los MSDN tiene varias artículos a cerca de ellos.


26
2017-09-02 13:19



Una implementación de lista o matriz carga todos los elementos de forma inmediata, mientras que la implementación de rendimiento proporciona una solución de ejecución diferida.

En la práctica, a menudo es deseable realizar la cantidad mínima de trabajo según sea necesario para reducir el consumo de recursos de una aplicación.

Por ejemplo, podemos tener una aplicación que procese millones de registros de una base de datos. Se pueden lograr los siguientes beneficios cuando utilizamos IEnumerable en un modelo de ejecución diferida basado en extracción:

  • Escalabilidad, fiabilidad y previsibilidad es probable que mejoren ya que la cantidad de registros no afecta significativamente los requisitos de recursos de la aplicación.
  • Rendimiento y capacidad de respuesta es probable que mejoren ya que el procesamiento puede comenzar inmediatamente en lugar de esperar a que se cargue por completo la colección completa.
  • Recuperabilidad y utilización es probable que mejoren ya que la aplicación puede detenerse, iniciarse, interrumpirse o fallar. Solo se perderán los elementos en progreso en comparación con la precarga de todos los datos donde solo se utilizó una parte de los resultados.
  • Procesamiento continuo es posible en entornos donde se agregan corrientes constantes de carga de trabajo.

Aquí hay una comparación entre compilar una colección primero, como una lista en comparación con el uso de rendimiento.

Ejemplo de lista

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Salida de consola
ContactListStore: Crear contacto 1
ContactListStore: Crear contacto 2
ContactListStore: Crear contacto 3
Listo para iterar a través de la colección.

Nota: toda la colección se cargó en la memoria sin siquiera pedir un solo elemento en la lista

Ejemplo de rendimiento

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Salida de consola
Listo para iterar a través de la colección.

Nota: La colección no se ejecutó en absoluto. Esto se debe a la naturaleza de "ejecución diferida" de IEnumerable. La construcción de un elemento solo ocurrirá cuando realmente se requiera.

Llamemos de nuevo a la colección y anulemos el comportamiento cuando buscamos el primer contacto en la colección.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Salida de consola
Listo para iterar a través de la colección
ContactYieldStore: Creación de contacto 1
Hola Bob

¡Bonito! Solo el primer contacto se construyó cuando el cliente "sacó" el elemento de la colección.


18
2017-12-04 19:52



Aquí hay una manera simple de entender el concepto: La idea básica es, si quieres una colección que puedas usar "foreach"on", pero reunir los elementos en la colección es costoso por alguna razón (como consultarlos desde una base de datos), Y a menudo no necesitarás toda la colección, luego creas una función que construye la colección un elemento a la vez y lo devuelve al consumidor (quien puede finalizar el esfuerzo de recolección temprano).

Piénsalo de esta manera: Vas al mostrador de la carne y quieres comprar una libra de jamón rebanado. El carnicero lleva un jamón de 10 libras a la parte de atrás, lo pone en la máquina cortadora, lo corta todo, luego trae la pila de rebanadas hacia usted y mide una libra. (Vieja forma). Con yield, el carnicero lleva la máquina cortadora al mostrador, y comienza a cortar y "ceder" cada rebanada a la balanza hasta que mida 1 libra, luego la enrolla por ti y listo. The Old Way puede ser mejor para el carnicero (le permite organizar su maquinaria como quiera), pero la Nueva Manera es claramente más eficiente en la mayoría de los casos para el consumidor.


13
2017-09-01 13:43



los yield palabra clave le permite crear un IEnumerable<T> en forma de bloque iterador. Este bloque iterador apoya ejecución diferida y si no está familiarizado con el concepto, puede parecer casi mágico. Sin embargo, al final del día, es solo un código que se ejecuta sin trucos extraños.

Un bloque iterador puede describirse como azúcar sintáctica donde el compilador genera una máquina de estado que realiza un seguimiento de hasta qué punto ha progresado la enumeración del enumerable. Para enumerar un enumerable, a menudo usa un foreach lazo. Sin embargo, un foreach loop es también azúcar sintáctico. Entonces, ustedes son dos abstracciones eliminadas del código real, y es por eso que inicialmente podría ser difícil entender cómo funciona todo junto.

Suponga que tiene un bloque iterador muy simple:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

Los bloques de iteradores reales a menudo tienen condiciones y bucles, pero cuando se comprueban las condiciones y se desenrollan los bucles, aún así terminan como yield declaraciones intercaladas con otro código.

Para enumerar el bloque iterador a foreach loop se usa:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Aquí está la salida (sin sorpresas aquí):

Empezar
1
Después de 1
2
Después de 2
42
Fin

Como se indicó anteriormente foreach es azúcar sintáctico:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

En un intento de desenredar esto, he creado un diagrama de secuencia con las abstracciones eliminadas:

C# iterator block sequence diagram

La máquina de estados generada por el compilador también implementa el enumerador, pero para que el diagrama sea más claro, los he mostrado como instancias separadas. (Cuando la máquina de estado se enumera a partir de otra cadena, en realidad se obtienen instancias separadas, pero ese detalle no es importante aquí).

Cada vez que llamas a tu bloque iterador, se crea una nueva instancia de la máquina de estado. Sin embargo, ninguno de sus códigos en el bloque iterador se ejecuta hasta enumerator.MoveNext() ejecuta por primera vez. Así es como funciona la ejecución diferida. Aquí hay un ejemplo (bastante tonto):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

En este punto, el iterador no se ha ejecutado. los Where cláusula crea un nuevo IEnumerable<T>eso envuelve el IEnumerable<T> devuelto por IteratorBlock pero este enumerable aún no se ha enumerado. Esto sucede cuando ejecutas un foreach lazo:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Si enumera el enumerable dos veces, se crea una nueva instancia de la máquina de estado cada vez y su bloque iterador ejecutará el mismo código dos veces.

Tenga en cuenta que los métodos LINQ les gusta ToList(), ToArray(), First(), Count() etc. usará un foreach loop para enumerar el enumerable. Por ejemplo ToList() enumerará todos los elementos enumerables y los almacenará en una lista. Ahora puede acceder a la lista para obtener todos los elementos enumerables sin que el bloque iterador se ejecute de nuevo. Existe una relación de compromiso entre el uso de la CPU para producir los elementos de la enumerable varias veces y la memoria para almacenar los elementos de la enumeración para acceder a ellos varias veces cuando se utilizan métodos como ToList().


9
2018-06-28 11:54



La palabra clave ceder C #, para decirlo simplemente, permite muchas llamadas a un cuerpo de código, conocido como iterador, que sabe cómo regresar antes de que esté hecho y, cuando se vuelve a llamar, continúa donde lo dejó, es decir, ayuda a un iterador ser transparente con estado por cada elemento en una secuencia que el iterador devuelve en llamadas sucesivas.

En JavaScript, el mismo concepto se llama Generadores.


8
2018-01-14 22:49