¿Para qué se usa la palabra clave de rendimiento en C #?

829

En la pregunta Cómo puedo exponer solo un fragmento de IList <> , 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é hace la palabra clave de rendimiento allí? Lo he visto referenciado en un par de lugares, y otra pregunta, pero aún no he descubierto lo que realmente hace. Estoy acostumbrado a pensar en el rendimiento en el sentido de que un hilo cede a otro, pero eso no parece relevante aquí.

Herms
fuente
Solo el enlace de MSDN al respecto está aquí msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx
Desarrollador el
14
Esto no es sorprendente. La confusión proviene del hecho de que estamos condicionados a ver el "retorno" como un resultado de la función, mientras que precedido por un "rendimiento" no lo es.
Larry
44
He leído los documentos, pero me temo que todavía no entiendo :(
Ortund

Respuestas:

737

La yieldpalabra clave realmente hace mucho aquí.

La función devuelve un objeto que implementa la IEnumerable<object>interfaz. Si una función de llamada comienza foreachsobre este objeto, la función se llama nuevamente hasta que "ceda". Este es el azúcar sintáctico introducido en C # 2.0 . En versiones anteriores, tenía que crear los suyos IEnumerabley los IEnumeratorobjetos 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. Intenta seguir este 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 pase por el ejemplo, encontrará la primera llamada a Integers()devoluciones 1. La segunda llamada regresa 2y la línea yield return 1no 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);
                }
            }
        }
    }
}
Mendelt
fuente
113
En este caso, eso sería más fácil, solo estoy usando el entero aquí para mostrar cómo funciona el rendimiento del rendimiento. Lo bueno de usar el rendimiento de rendimiento es que es una forma muy rápida de implementar el patrón iterador, por lo que las cosas se evalúan perezosamente.
Mendelt
111
También vale la pena señalar que puede usar yield break;cuando no desea devolver más artículos.
Rory
77
yieldNo es una palabra clave. Si fuese entonces no podía usar rendimiento como un identificador como enint yield = 500;
Brandin
55
@Brandin es porque todos los lenguajes de programación admiten dos tipos de palabras clave, a saber, reservadas y contextuales. el rendimiento cae en la categoría posterior, razón por la cual su código no está prohibido por el compilador de C #. Más detalles aquí: ericlippert.com/2009/05/11/reserved-and-contextual-keywords Le encantaría saber que también hay palabras reservadas que un idioma no reconoce como palabras clave. Por ejemplo, goto en java. Más detalles aquí: stackoverflow.com/questions/2545103/…
RBT
77
'If a calling function starts foreach-ing over this object the function is called again until it "yields"'. no me suena bien Siempre pensé en la palabra clave c # yield en el contexto de "el cultivo produce una cosecha abundante", en lugar de "el automóvil rinde al peatón".
Zack
369

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

Joel Coehoorn
fuente
210

El rendimiento tiene dos grandes usos,

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

  2. Ayuda a hacer iteraciones con estado. ingrese la descripción de la imagen aquí

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

Shivprasad Koirala
fuente
13
El video me ayuda a entender claramente el yield. Artículo del proyecto de código de @ ShivprasadKoirala ¿Cuál es el uso de C # Yield? de la misma explicación también es una buena fuente
Dush
También agregaría como tercer punto que yieldes una forma "rápida" de crear un IEnumerator personalizado (en lugar de que una clase implemente la interfaz IEnumerator).
MrTourkos
Vi su video Shivprasad y explicaba claramente el uso de la palabra clave de rendimiento.
Tore Aurstad
¡Gracias por el video!. Muy bien explicado!
Roblogic
Gran video, pero me pregunto ... La implementación que usa el rendimiento es obviamente más limpia, pero esencialmente debe estar creando su propia memoria temporal o Lista internamente para hacer un seguimiento del estado (o más bien crear una máquina de estado). Entonces, ¿está "Yield" haciendo algo más que simplificar la implementación y hacer que las cosas se vean mejor o hay algo más? ¿Qué hay de la eficiencia? ¿Ejecutar código usando Yield es más o menos eficiente / rápido que sin él?
toughQuestions
135

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

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

Svend
fuente
Esto necesita ser votado. Dulce cómo explica el propósito del operador y las partes internas.
sajidnizami
3
la parte 1 explica el azúcar sintáctico del "rendimiento del rendimiento". excelente explicación!
Dror Weiss
99

A primera vista, el rendimiento del 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 el 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 solicite. Esa es una buena razón para usarlo.

Marquinho Peli
fuente
40

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

Tratemos de 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 al Fibs()método y luego el estado se restablecerá.

RKS
fuente
2
set prevFib = 1 - el primer número de Fibonacci es un "1", no un "0"
fubo
31

Intuitivamente, la palabra clave devuelve un valor de la función sin abandonarla, es decir, en su ejemplo de código, devuelve el itemvalor actual y luego reanuda el ciclo. Más formalmente, el compilador lo utiliza para generar código para un iterador . Los iteradores son funciones que devuelven IEnumerableobjetos. El MSDN tiene varios artículos sobre ellos.

Konrad Rudolph
fuente
44
Bueno, para ser precisos, no reanuda el ciclo, lo pausa hasta que el padre llama "iterator.next ()".
Alex
8
@jitbit Es por eso que usé "intuitivamente" y "más formalmente".
Konrad Rudolph
31

Una implementación de lista o matriz carga todos los elementos inmediatamente, 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. Los siguientes beneficios se pueden lograr cuando usamos IEnumerable en un modelo basado en extracción de ejecución diferida:

  • Es probable que la escalabilidad, la confiabilidad y la previsibilidad mejoren, ya que la cantidad de registros no afecta significativamente los requisitos de recursos de la aplicación.
  • Es probable que el rendimiento y la capacidad de respuesta mejoren, ya que el procesamiento puede comenzar de inmediato en lugar de esperar a que se cargue primero toda la colección.
  • Es probable que la recuperación y la utilización mejoren, ya que la aplicación se puede detener, iniciar, interrumpir o fallar. Solo se perderán los elementos en progreso en comparación con la búsqueda previa de todos los datos donde solo se usó una parte de los resultados.
  • El procesamiento continuo es posible en entornos donde se agregan flujos de carga de trabajo constante.

Aquí hay una comparación entre construir 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: creación de contacto 1
ContactListStore: creación de contacto 2
ContactListStore: creación de 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
Lista para iterar por 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 artículo solo ocurrirá cuando sea realmente necesario.

Volvamos a llamar a la colección y anulemos el comportamiento cuando busquemos el primer contacto de 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
Lista para iterar a través de la colección
ContactYieldStore: creación de contacto 1
Hola Bob

¡Agradable! Solo se construyó el primer contacto cuando el cliente "sacó" el artículo de la colección.

Strydom
fuente
1
¡Esta respuesta necesita más atención! Thx
leon22
@ leon22 absolutamente +2
snr
26

Aquí hay una manera simple de entender el concepto: la idea básica es, si desea una colección en la que pueda usar " foreach", 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á la colección completa, luego crea una función que construye la colección un elemento a la vez y la devuelve al consumidor (que luego puede finalizar el esfuerzo de recolección antes).

Piénsalo de esta manera: vas al mostrador de carne y quieres comprar una libra de jamón rebanado. El carnicero lleva un jamón de 10 libras a la parte posterior, lo pone en la máquina rebanadora, lo corta todo, luego le devuelve el montón de rebanadas y mide una libra de él. (Vieja forma). Con yield, el carnicero lleva la máquina rebanadora al mostrador, y comienza a rebanar y "ceder" cada rebanada en la balanza hasta que mida 1 libra, luego la envuelve y listo. El Old Way puede ser mejor para el carnicero (le permite organizar su maquinaria de la manera que quiera), pero el New Way es claramente más eficiente en la mayoría de los casos para el consumidor.

kmote
fuente
18

La yieldpalabra clave le permite crear un IEnumerable<T>formulario en un bloque iterador . Este bloque iterador admite la ejecución diferida y, si no está familiarizado con el concepto, puede parecer casi mágico. Sin embargo, al final del día, solo el código se ejecuta sin ningún truco extraño.

Un bloque iterador puede describirse como azúcar sintáctico en el que el compilador genera una máquina de estados que realiza un seguimiento de hasta qué punto ha progresado la enumeración de lo enumerable. Para enumerar un enumerable, a menudo usa un foreachbucle. Sin embargo, un foreachbucle también es azúcar sintáctico. Entonces, son dos abstracciones eliminadas del código real, por lo 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 iteradores reales a menudo tienen condiciones y bucles, pero cuando verifica las condiciones y desenrolla los bucles, terminan siendo yielddeclaraciones intercaladas con otro código.

Para enumerar el iterador, bloquee un foreach se usa bucle:

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

Aquí está la salida (no hay sorpresas aquí):

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

Como se indicó anteriormente foreaches el 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:

Diagrama de secuencia de bloques del iterador C #

La máquina de estado 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 desde otro subproceso, en realidad se obtienen instancias separadas, pero ese detalle no es importante aquí).

Cada vez que llama a su 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 que se enumerator.MoveNext()ejecute 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. La Wherecláusula crea un nuevo IEnumerable<T>que envuelve el IEnumerable<T>devuelto por IteratorBlockpero este enumerable aún no se ha enumerado. Esto sucede cuando ejecuta un foreachbucle:

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 de LINQ gusta ToList(), ToArray(), First(), Count()etc. va a utilizar un foreachbucle para enumerar la 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 del enumerable sin que el bloque iterador se ejecute nuevamente. Existe una compensación entre el uso de CPU para producir los elementos de los enumerables múltiples 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().

Martin Liversage
fuente
18

Si entiendo esto correctamente, así es como lo expresaría desde la perspectiva de la función que implementa IEnumerable con rendimiento.

  • Aquí hay uno.
  • Llame nuevamente si necesita otro.
  • Recordaré lo que ya te di.
  • Solo sabré si puedo darte otro cuando vuelvas a llamar.
Casey Plummer
fuente
simple y brillante
Harry
10

La palabra clave de rendimiento C #, en pocas palabras, permite muchas llamadas a un cuerpo de código, conocido como iterador, que sabe cómo regresar antes de que se haga y, cuando se llama nuevamente, continúa donde se quedó, es decir, ayuda a un iterador se vuelve transparente con estado por cada elemento en una secuencia que el iterador devuelve en llamadas sucesivas.

En JavaScript, el mismo concepto se llama Generadores.

BoiseBaked
fuente
La mejor explicación aún. ¿Son estos también los mismos generadores en Python?
petrosmm
7

Es una manera muy simple y fácil de crear un enumerable para su objeto. El compilador crea una clase que envuelve su método y que implementa, en este caso, IEnumerable <object>. Sin la palabra clave de rendimiento, tendría que crear un objeto que implemente IEnumerable <object>.


fuente
5

Está produciendo una secuencia enumerable. Lo que hace es crear una secuencia local IEnumerable y devolverla como resultado de un método

aku
fuente
3

Este enlace tiene un ejemplo simple

Incluso ejemplos más simples están aquí

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Tenga en cuenta que el rendimiento no regresará del método. Incluso puedes poner un WriteLinedespués delyield return

Lo anterior produce un IEnumerable de 4 ints 4,4,4,4

Aquí con un WriteLine. Agregará 4 a la lista, imprimirá abc, luego agregará 4 a la lista, luego completará el método y así volverá realmente del método (una vez que el método se haya completado, como sucedería con un procedimiento sin retorno). Pero esto tendría un valor, una IEnumerablelista de ints, que devuelve al finalizar.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Observe también que cuando usa el rendimiento, lo que está devolviendo no es del mismo tipo que la función. Es del tipo de un elemento dentro de la IEnumerablelista.

Utiliza el rendimiento con el tipo de retorno del método como IEnumerable. Si el tipo de retorno del método es into List<int>y usted usa yield, entonces no se compilará. Puede usar el IEnumerabletipo de retorno del método sin rendimiento, pero parece que tal vez no pueda usar el rendimiento sin el IEnumerabletipo de retorno del método.

Y para que se ejecute, debe llamarlo de una manera especial.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}
barlop
fuente
nota: si intenta comprender SelectMany, usa rendimiento y también genéricos ... este ejemplo puede ayudar public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }y public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); }
barlop,
Parece una muy buena explicación! Esta podría haber sido la respuesta aceptada.
pongapundit
@pongapundit gracias, mi respuesta es clara y simple, pero yo mismo no he usado mucho, otros respondedores tienen mucha más experiencia y conocimiento de sus usos que yo. ¡Lo que escribí sobre rendimiento aquí fue probablemente por rascarme la cabeza tratando de descubrir algunas de las respuestas aquí y en ese enlace dotnetperls! Pero como no lo sé yield returnbien (aparte de lo simple que mencioné), y no lo he usado mucho y no sé mucho sobre sus usos, no creo que este sea el aceptado.
barlop
3

Un punto importante sobre la palabra clave Yield es Lazy Execution . Ahora lo que quiero decir con Lazy Execution es ejecutar cuando sea necesario. Una mejor manera de decirlo es dando un ejemplo

Ejemplo: no se utiliza Yield, es decir, no hay ejecución diferida.

        public static IEnumerable<int> CreateCollectionWithList()
        {
            var list =  new List<int>();
            list.Add(10);
            list.Add(0);
            list.Add(1);
            list.Add(2);
            list.Add(20);

            return list;
        }

Ejemplo: uso de rendimiento, es decir, ejecución diferida.

    public static IEnumerable<int> CreateCollectionWithYield()
    {
        yield return 10;
        for (int i = 0; i < 3; i++) 
        {
            yield return i;
        }

        yield return 20;
    }

Ahora cuando llamo a ambos métodos.

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

notará que listItems tendrá 5 elementos dentro (desplace el mouse sobre listItems durante la depuración). Mientras que yieldItems solo tendrá una referencia al método y no a los elementos. Eso significa que no ha ejecutado el proceso de obtener elementos dentro del método. Una forma muy eficiente de obtener datos solo cuando es necesario. La implementación real del rendimiento se puede ver en ORM como Entity Framework y NHibernate, etc.

maxspan
fuente
-3

Está tratando de aportar algo de Ruby Goodness :)
Concepto: este es un ejemplo de código Ruby que imprime cada elemento de la matriz

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

La implementación de cada método de la matriz le da el control a la persona que llama (el 'pone x') con cada elemento de la matriz perfectamente presentado como x. La persona que llama puede hacer lo que tenga que hacer con x.

Sin embargo, .Net no llega hasta aquí ... C # parece haber unido el rendimiento con IEnumerable, de tal forma que te obliga a escribir un bucle foreach en la persona que llama como se ve en la respuesta de Mendelt. Poco menos elegante.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}
Gishu
fuente
77
-1 Esta respuesta no me suena bien. Sí, C # yieldestá asociado IEnumerabley C # carece del concepto de Ruby de "bloque". Pero C # tiene lambdas, lo que podría permitir la implementación de un ForEachmétodo, muy similar al de Ruby each. Sin embargo, esto no significa que sea una buena idea hacerlo .
rsenna
Mejor aún: public IEnumerable <int> Each () {int index = 0; datos de retorno de rendimiento [index ++]; }
ata