c# - valores - return en c
¿Cuál es el propósito/ventaja de usar iteradores de rendimiento de retorno en C#? (10)
Todos los ejemplos que he visto de usar yield return x;
dentro de un método C # podría hacerse de la misma manera simplemente devolviendo la lista completa. En esos casos, ¿hay algún beneficio o ventaja en el uso de la sintaxis de yield return
frente a la devolución de la lista?
Además, ¿en qué tipos de escenarios se utilizaría el yield return
que no podría simplemente devolver la lista completa?
Evaluación diferida / Ejecución diferida
Los bloques de iterador de "rendimiento devuelto" no ejecutarán ninguno de los códigos hasta que realmente solicite ese resultado específico. Esto significa que también se pueden unir de manera eficiente. Pop Quiz: suponiendo que la función "ReadLines ()" lee todas las líneas de un archivo de texto y se implementa mediante un bloque iterador, ¿cuántas veces el siguiente código iterará sobre el archivo?
var query = 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 eso no hasta muy abajo en el ciclo foreach
.
Separación de intereses
Utilizando nuevamente la función hipotética ReadLines()
de arriba, 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.
Listas infinitas
Vea mi respuesta a esta pregunta para un buen ejemplo:
Errores de retorno de la función C # Fibonacci
Básicamente, implemento la secuencia de fibonacci usando un bloque de iteradores que nunca se detendrá (al menos, antes de llegar a MaxInt), y luego usaré esa implementación de manera segura.
Semántica mejorada
Esta es una de esas cosas que es mucho más difícil de explicar con la prosa de lo que es simplemente quién con un simple visual 1 :
Si no puede ver la imagen, muestra dos versiones del mismo código, con destacados de fondo para diferentes inquietudes. El código linq tiene todos los colores muy 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 vs usando 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.
A veces, las secuencias que necesita devolver son demasiado grandes para caber en la memoria. Por ejemplo, hace aproximadamente 3 meses participé en un proyecto de migración de datos entre bases de datos MS SLQ. Los datos fueron exportados en formato XML. El retorno del rendimiento resultó ser bastante útil con XmlReader . Hizo la programación bastante más fácil. Por ejemplo, supongamos que un archivo tiene 1000 elementos de cliente : si solo lee este archivo en la memoria, esto requerirá almacenarlos todos en la memoria al mismo tiempo, incluso si se manejan secuencialmente. Por lo tanto, puede usar iteradores para recorrer la colección uno por uno. En ese caso, debe gastar solo memoria para un elemento.
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 colgó todo el sistema y no generó OutOfMemoryException . Por supuesto, puede trabajar con XmlReader sin iteradores de rendimiento. Pero los iteradores hicieron mi vida mucho más fácil (no escribiría el código para importar tan rápido y sin problemas). Mire esta page para ver cómo los iteradores de rendimiento se usan para resolver problemas reales (no solo científicos con secuencias infinitas).
Aquí está mi respuesta aceptada anterior a exactamente la misma pregunta:
¿Valor agregado de la palabra clave de rendimiento?
Otra forma de ver los métodos iterator es que hacen el trabajo duro de convertir un algoritmo "de adentro hacia afuera". Considera 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 que esto sea fácil para mí como autor analizador al tomar el enfoque SAX, en el cual tengo una interfaz de devolución de llamada que notifico cada vez que encuentro la siguiente pieza del patrón. Entonces en el caso de SAX, cada vez que encuentro el comienzo de un elemento, llamo al método beginElement
, y así sucesivamente.
Pero esto crea problemas para mis usuarios. Deben implementar la interfaz del controlador y deben escribir una clase de máquina de estado que responda a los métodos de devolución de llamada. Es difícil hacerlo bien, así que lo más fácil es usar una implementación de stock que construya un árbol DOM, y luego tendrán la comodidad de poder caminar por el árbol. Pero luego toda la estructura se almacena en la memoria, no es buena.
¿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 interfaz de devolución de llamada: solo devuelve un objeto derivado de mi clase base LanguageElement
lugar de llamar a un método de devolución de llamada.
El usuario ahora puede usar foreach para realizar un bucle a través de 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 estar bajo control y, por lo tanto, son más fáciles de escribir y comprender.
Echa un vistazo a esta discusión en el blog de Eric White (excelente blog por cierto) sobre evaluación perezosa versus entusiasta .
En los escenarios de juguete / demostración, no hay mucha diferencia. Pero hay situaciones en las que los iteradores flexibles son útiles; a veces, la lista completa no está disponible (por ejemplo, las transmisiones), o la lista es computacionalmente costosa y es poco probable que se necesite en su totalidad.
La razón básica para usar el rendimiento es que genera / devuelve una lista por sí mismo. Podemos usar la lista devuelta para iterar más.
Las buenas respuestas aquí sugieren que un beneficio del yield return
de yield return
es que no necesita crear una lista ; Las listas pueden ser costosas. (Además, después de un tiempo, los encontrará voluminosos y poco elegantes.)
Pero, ¿y si no tienes una lista?
yield return
permite recorrer estructuras de datos (no necesariamente Listas) de varias maneras. Por ejemplo, si su objeto es un Árbol, puede recorrer los nodos en orden previa 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;
}
Pero, ¿y si estuvieras construyendo una colección tú mismo?
En general, los iteradores se pueden usar para generar perezadamente una secuencia de objetos . Por ejemplo, el método Enumerable.Range
no tiene ningún tipo de colección internamente. Simplemente genera el próximo número a pedido . Hay muchos usos para esta generación de secuencias perezosas usando una máquina de estados. La mayoría de ellos están cubiertos por conceptos de programación funcional .
En mi opinión, si miras los iteradores solo como una manera de enumerar a través de una colección (es solo uno de los casos de uso más simples), vas por el camino equivocado. Como dije, los iteradores son medios para devolver secuencias. La secuencia puede ser infinita . No habría forma de devolver una lista con una longitud infinita y usar los primeros 100 elementos. Tiene que ser flojo a veces. Devolver una colección es considerablemente diferente de devolver un generador de colecciones (que es lo que es un iterador). Está comparando 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 de menos de un millón sin tocar el algoritmo de generación de números primos en absoluto. 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 elementos desea desde el principio.
Si toda la lista es gigantesca, podría consumir mucha memoria solo para sentarse, mientras que con el rendimiento solo jugará con lo que necesita, cuando lo necesite, independientemente de la cantidad de elementos que haya.
Usando el yield return
puede iterar sobre los artículos sin tener que construir una lista. Si no necesita la lista, pero desea repetir un conjunto de elementos, puede ser más fácil 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 operar foo dentro de su método utilizando rentabilidades de rendimiento y su bucle foreach puede ser mucho más conciso.