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 return
sintaxis frente a devolver la lista?
Además, ¿en qué tipos de escenarios se yield return
utilizarían en los que no podría simplemente devolver la lista completa?
c#
iterator
yield-return
CoderDennis
fuente
fuente
yield
s es que no es necesario nombrar otra variable intermedia.Respuestas:
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.Range
mé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.
fuente
Las buenas respuestas aquí sugieren que uno de los beneficios
yield return
es 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 return
le 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; }
fuente
yield!
la forma en que lo hace F # para que no necesite todas lasforeach
declaraciones.yield return
: a menudo no es obvio cuándo producirá un código eficiente o ineficiente. Aunqueyield return
se 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.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
foreach
ciclo. 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 :
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.
fuente
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).
fuente
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.
fuente
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.
fuente
Eche un vistazo a esta discusión en el blog de Eric White (excelente blog por cierto) sobre evaluación perezosa versus ansiosa .
fuente
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 escribirforeach (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.
fuente
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
beginElement
mé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
LanguageElement
clase 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.
fuente
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.
fuente
return yield
no genera una lista, solo genera el siguiente elemento de la lista y solo cuando se solicita (se repite).