¿Cuál es el propósito / ventaja de usar iteradores de retorno de rendimiento en C #?


80

Todos los ejemplos que he visto de uso yield return x;dentro de un método C # se pueden hacer de la misma manera simplemente devolviendo la lista completa. En esos casos, ¿hay algún beneficio o ventaja en utilizar la yield returnsintaxis frente a devolver la lista?

Además, ¿en qué tipos de escenarios se yield returnutilizarían en los que no podría simplemente devolver la lista completa?


15
¿Por qué asume que hay "una lista" en primer lugar? ¿Y si no lo hay?
Eric Lippert

2
@ Eric, supongo que eso es lo que estaba preguntando. ¿Cuándo no tendrías una lista en primer lugar? Los flujos de archivos y las secuencias infinitas son 2 excelentes ejemplos en las respuestas hasta ahora.
CoderDennis

1
Si tiene la lista, entonces, por supuesto, devuélvala; pero si está creando una lista dentro del método y devolviéndola, entonces podría / debería usar iteradores en su lugar. Entrega los artículos uno a la vez. Hay muchos beneficios.
justin.m.chase

2
¡Ciertamente he aprendido mucho desde que hice esta pregunta hace 5 años!
CoderDennis

1
La mayor ventaja de tener yields es que no es necesario nombrar otra variable intermedia.
nawfal

Respuestas:


120

Pero, ¿y si estuvieras construyendo una colección tú mismo?

En general, los iteradores se pueden utilizar para generar de forma perezosa una secuencia de objetos . Por ejemplo el Enumerable.Rangemétodo no tiene ningún tipo de colección internamente. Simplemente genera el siguiente número a pedido . Hay muchos usos para esta generación de secuencia perezosa usando una máquina de estado. La mayoría de ellos están cubiertos por conceptos de programación funcional .

En mi opinión, si está buscando iteradores solo como una forma de enumerar a través de una colección (es solo uno de los casos de uso más simples), está yendo por el camino equivocado. Como dije, los iteradores son medios para devolver secuencias. La secuencia podría incluso ser infinita . No habría forma de devolver una lista con una longitud infinita y usar los primeros 100 elementos. A veces tiene que ser perezoso. Devolver una colección es considerablemente diferente de devolver un generador de colección (que es lo que es un iterador). Es comparar manzanas con naranjas.

Ejemplo hipotético:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

Este ejemplo imprime números primos menores que 10000. Puede cambiarlo fácilmente para imprimir números menores a un millón sin tocar el algoritmo de generación de números primos. En este ejemplo, no puede devolver una lista de todos los números primos porque la secuencia es infinita y el consumidor ni siquiera sabe cuántos artículos quiere desde el principio.


Correcto. He creado la lista, pero ¿qué diferencia tiene devolver un artículo a la vez frente a devolver la lista completa?
CoderDennis

4
Entre otras razones, hace que su código sea más modular para que pueda cargar un elemento, procesarlo y luego repetirlo. Además, considere el caso en el que cargar un artículo es muy caro, o hay muchos (dicen millones). En esos casos, no es deseable cargar la lista completa.
Dana the Sane

15
@Dennis: Para una lista almacenada linealmente en la memoria, puede que no tenga una diferencia, pero si estuviera, por ejemplo, enumerando un archivo de 10GB y procesando cada línea una por una, haría una diferencia.
mmx

1
+1 para obtener una respuesta excelente: también agregaría que la palabra clave yield permite que la semántica del iterador se aplique a fuentes que no se consideran tradicionalmente colecciones, como sockets de red, servicios web o incluso problemas de concurrencia (consulte stackoverflow.com/questions/ 481714 / ccr-yield-and-vb-net )
LBushkin

Buen ejemplo, así que básicamente es un generador de colección que se basa en el contexto (por ejemplo, llamada al método) y no entra en acción hasta que algo intenta acceder a él, mientras que un método de colección tradicional sin rendimiento necesitaría saber su tamaño para construir y devolver una colección completa, luego iterar sobre la parte requerida de esa colección?
Michael Harper

24

Las buenas respuestas aquí sugieren que uno de los beneficios yield returnes que no es necesario crear una lista ; Las listas pueden resultar caras. (Además, después de un tiempo, los encontrará voluminosos y poco elegantes).

Pero, ¿y si no tienes una lista?

yield returnle permite atravesar estructuras de datos (no necesariamente Listas) de varias formas. Por ejemplo, si su objeto es un árbol, puede recorrer los nodos en orden previo o posterior sin crear otras listas o cambiar la estructura de datos subyacente.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

1
Este ejemplo también destaca el caso de la delegación. Si tiene una colección que, en determinadas circunstancias, podría contener elementos de otras colecciones, es muy sencillo iterar y utilizar el rendimiento de rendimiento en lugar de crear una lista completa de todos los resultados y devolverla.
Tom Mayfield

1
Ahora C # solo necesita implementar yield!la forma en que lo hace F # para que no necesite todas las foreachdeclaraciones.
CoderDennis

Por cierto, su ejemplo muestra uno de los "peligros" de yield return: a menudo no es obvio cuándo producirá un código eficiente o ineficiente. Aunque yield returnse puede usar de forma recursiva, dicho uso impondrá una sobrecarga significativa en el procesamiento de enumeradores profundamente anidados. La administración de estado manual puede ser más complicada de codificar, pero se ejecuta de manera mucho más eficiente.
supercat

17

Evaluación diferida / ejecución diferida

Los bloques del iterador "rendimiento de retorno" no ejecutarán ninguno de los códigos hasta que realmente solicite ese resultado específico. Esto significa que también se pueden encadenar juntos de manera eficiente. Examen sorpresa: ¿cuántas veces se repetirá el siguiente código sobre el archivo?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

La respuesta es exactamente una, y no hasta el final del foreachciclo. Aunque tengo tres funciones de operador linq separadas, solo recorremos el contenido del archivo una vez.

Esto tiene otros beneficios además del rendimiento. Por ejemplo, puedo escribir un método bastante simple y genérico para leer y prefiltrar un archivo de registro una vez, y usar ese mismo método en varios lugares diferentes, donde cada uso agrega filtros diferentes. Por lo tanto, mantengo un buen rendimiento al mismo tiempo que reutilizo el código de manera eficiente.

Listas infinitas

Vea mi respuesta a esta pregunta para ver un buen ejemplo:
C # función fibonacci que devuelve errores

Básicamente, implemento la secuencia de fibonacci usando un bloque iterador que nunca se detendrá (al menos, no antes de llegar a MaxInt), y luego uso esa implementación de una manera segura.

Semántica mejorada y separación de preocupaciones

Una vez más, utilizando el ejemplo de archivo anterior, ahora podemos separar fácilmente el código que lee el archivo del código que filtra las líneas innecesarias del código que realmente analiza los resultados. Ese primero, especialmente, es muy reutilizable.

Esta es una de esas cosas que es mucho más difícil de explicar con prosa que a quién con una simple imagen visual 1 :

Separación de preocupaciones imperativa vs funcional

Si no puede ver la imagen, muestra dos versiones del mismo código, con resaltados de fondo para diferentes preocupaciones. El código linq tiene todos los colores bien agrupados, mientras que el código imperativo tradicional tiene los colores entremezclados. El autor argumenta (y estoy de acuerdo) que este resultado es típico de usar linq versus usar código imperativo ... que linq hace un mejor trabajo organizando su código para tener un mejor flujo entre las secciones.


1 Creo que esta es la fuente original: https://twitter.com/mariofusco/status/571999216039542784 . También tenga en cuenta que este código es Java, pero el C # sería similar.


1
La ejecución diferida es probablemente el mayor beneficio de los iteradores.
justin.m.chase

12

A veces, las secuencias que necesita devolver son demasiado grandes para caber en la memoria. Por ejemplo, hace unos 3 meses participé en un proyecto de migración de datos entre bases de datos MS SLQ. Los datos se exportaron en formato XML. El rendimiento del rendimiento resultó ser bastante útil con XmlReader . Hizo la programación bastante más fácil. Por ejemplo, suponga que un archivo tiene 1000 elementos Customer ; si acaba de leer este archivo en la memoria, será necesario almacenarlos todos en la memoria al mismo tiempo, incluso si se manejan secuencialmente. Entonces, puede usar iteradores para recorrer la colección uno por uno. En ese caso, solo debe gastar memoria para un elemento.

Al final resultó que, usar XmlReader para nuestro proyecto era la única manera de hacer que la aplicación funcionara; funcionó durante mucho tiempo, pero al menos no bloqueó todo el sistema y no generó OutOfMemoryException . Por supuesto, puede trabajar con XmlReader sin iteradores de rendimiento. Pero los iteradores me hicieron la vida mucho más fácil (no escribiría el código para importar tan rápido y sin problemas). Mire esta página para ver cómo se utilizan los iteradores de rendimiento para resolver problemas reales (no solo científicos con secuencias infinitas).


9

En escenarios de juguete / demostración, no hay mucha diferencia. Pero hay situaciones en las que los iteradores de rendimiento son útiles; a veces, la lista completa no está disponible (por ejemplo, flujos) o la lista es computacionalmente costosa y es poco probable que se necesite en su totalidad.


2

Si la lista completa es gigantesca, es posible que consuma mucha memoria solo para sentarse, mientras que con el rendimiento solo juega con lo que necesita, cuando lo necesita, independientemente de cuántos elementos haya.



2

Con el yield return, puede iterar sobre elementos sin tener que crear una lista. Si no necesita la lista, pero desea iterar sobre algún conjunto de elementos, puede ser más fácil de escribir

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Que

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Puede poner toda la lógica para determinar si desea o no operar en foo dentro de su método utilizando rendimientos de rendimiento y cada ciclo puede ser mucho más conciso.


2

Aquí está mi anterior respuesta aceptada a exactamente la misma pregunta:

¿Rendimiento del valor agregado de la palabra clave?

Otra forma de ver los métodos de iterador es que hacen el arduo trabajo de darle la vuelta a un algoritmo. Considere un analizador. Extrae texto de una secuencia, busca patrones en ella y genera una descripción lógica de alto nivel del contenido.

Ahora, puedo hacer esto fácil para mí como autor del analizador si adopto el enfoque SAX, en el que tengo una interfaz de devolución de llamada a la que notifico cada vez que encuentro la siguiente pieza del patrón. Entonces, en el caso de SAX, cada vez que encuentro el inicio de un elemento, llamo al beginElementmétodo, y así sucesivamente.

Pero esto crea problemas para mis usuarios. Tienen que implementar la interfaz del controlador y, por lo tanto, tienen que escribir una clase de máquina de estado que responda a los métodos de devolución de llamada. Esto es difícil de hacer bien, por lo que lo más fácil es usar una implementación estándar que construya un árbol DOM, y luego tendrán la conveniencia de poder caminar por el árbol. Pero luego toda la estructura se almacena en la memoria, no es bueno.

Pero, ¿qué tal si escribo mi analizador como un método iterador?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Eso no será más difícil de escribir que el enfoque de la interfaz de devolución de llamada: solo devuelva un objeto derivado de mi LanguageElementclase base en lugar de llamar a un método de devolución de llamada.

El usuario ahora puede usar foreach para recorrer la salida de mi analizador, por lo que obtiene una interfaz de programación imperativa muy conveniente.

El resultado es que ambos lados de una API personalizada parecen tener el control y, por lo tanto, son más fáciles de escribir y comprender.


2

La razón básica para usar yield es que genera / devuelve una lista por sí mismo. Podemos usar la lista devuelta para iterar más.


Conceptualmente correcto pero técnicamente incorrecto. Devuelve una instancia de IEnumerable que simplemente abstrae un iterador. Ese iterador es realmente lógico para obtener el siguiente elemento y no una lista materializada. El uso return yieldno genera una lista, solo genera el siguiente elemento de la lista y solo cuando se solicita (se repite).
Sinaesthetic
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.