waitforseconds que c# .net yield yield-return

c# - que - Cuándo NO usar yield(return)



yield return c# stack overflow (11)

Esta pregunta ya tiene una respuesta aquí:
¿Alguna vez hay una razón para no usar ''yield return'' al devolver un IEnumerable?

Aquí hay varias preguntas útiles sobre los beneficios del yield return . Por ejemplo,

Estoy buscando ideas sobre cuándo NO usar el yield return . Por ejemplo, si espero tener que devolver todos los artículos de una colección, no parece que el yield sea ​​útil, ¿no?

¿Cuáles son los casos en que el uso del yield será limitante, innecesario, me meterá en problemas o debería evitarse?


¿Cuáles son los casos en que el uso del rendimiento será limitante, innecesario, me meterá en problemas o debería evitarse?

Es una buena idea pensar cuidadosamente sobre el uso del "retorno de rendimiento" cuando se trata de estructuras definidas recursivamente. Por ejemplo, a menudo veo esto:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root) { if (root == null) yield break; yield return root.Value; foreach(T item in PreorderTraversal(root.Left)) yield return item; foreach(T item in PreorderTraversal(root.Right)) yield return item; }

Código de apariencia sensata, pero tiene problemas de rendimiento. Supongamos que el árbol tiene h de profundidad. Entonces, en la mayoría de los puntos habrá O (h) iteradores anidados construidos. Llamar a "MoveNext" en el iterador externo hará O (h) llamadas anidadas a MoveNext. Como hace O (n) veces para un árbol con n elementos, eso hace que el algoritmo O (hn). Y dado que la altura de un árbol binario es lg n <= h <= n, eso significa que el algoritmo es en el mejor de los casos O (n lg n) y en el peor O (n ^ 2) en el tiempo, y el mejor caso O (lg n) y peor caso O (n) en el espacio de la pila. Es O (h) en el espacio de montón porque cada enumerador se asigna en el montón. (En las implementaciones de C # que conozco, una implementación conforme puede tener otras características de pila o espacio dinámico).

Pero iterar un árbol puede ser O (n) en el tiempo y O (1) en el espacio de la pila. Puede escribir esto en su lugar como:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root) { var stack = new Stack<Tree<T>>(); stack.Push(root); while (stack.Count != 0) { var current = stack.Pop(); if (current == null) continue; yield return current.Value; stack.Push(current.Left); stack.Push(current.Right); } }

que aún utiliza el retorno de rendimiento, pero es mucho más inteligente al respecto. Ahora somos O (n) en el tiempo y O (h) en el espacio del montón, y O (1) en el espacio de la pila.

Lectura adicional: ver el artículo de Wes Dyer sobre el tema:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx


¿Cuáles son los casos en que el uso del rendimiento será limitante, innecesario, me meterá en problemas o debería evitarse?

Puedo pensar en un par de casos, IE:

  • Evite usar el retorno de rendimiento cuando devuelve un iterador existente. Ejemplo:

    // Don''t do this, it creates overhead for no reason // (a new state machine needs to be generated) public IEnumerable<string> GetKeys() { foreach(string key in _someDictionary.Keys) yield return key; } // DO this public IEnumerable<string> GetKeys() { return _someDictionary.Keys; }

  • Evite utilizar el retorno de rendimiento cuando no desee diferir el código de ejecución para el método. Ejemplo:

    // Don''t do this, the exception won''t get thrown until the iterator is // iterated, which can be very far away from this method invocation public IEnumerable<string> Foo(Bar baz) { if (baz == null) throw new ArgumentNullException(); yield ... } // DO this public IEnumerable<string> Foo(Bar baz) { if (baz == null) throw new ArgumentNullException(); return new BazIterator(baz); }


Aquí hay muchas respuestas excelentes. Añadiría este: No use el retorno de rendimiento para colecciones pequeñas o vacías donde ya conoce los valores:

IEnumerable<UserRight> GetSuperUserRights() { if(SuperUsersAllowed) { yield return UserRight.Add; yield return UserRight.Edit; yield return UserRight.Remove; } }

En estos casos, la creación del objeto Enumerator es más costosa y más detallada que simplemente generar una estructura de datos.

IEnumerable<UserRight> GetSuperUserRights() { return SuperUsersAllowed ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove} : Enumerable.Empty<UserRight>(); }

Actualizar

Aquí están los resultados de mi punto de referencia :

Estos resultados muestran cuánto tiempo tomó (en milisegundos) realizar la operación 1,000,000 de veces. Los números más pequeños son mejores.

Al revisar esto, la diferencia de rendimiento no es lo suficientemente importante como para preocuparse, por lo que debe ir con lo que sea más fácil de leer y mantener.


Cuando no quiere que un bloque de código devuelva un iterador para el acceso secuencial a una colección subyacente, no necesita yield return . Simplemente return la colección entonces.


El rendimiento sería limitante / innecesario cuando necesite acceso aleatorio. Si necesita acceder al elemento 0 y luego al elemento 99, habrá eliminado la utilidad de la evaluación perezosa.


Eric Lippert plantea un buen punto (es una lástima que C # no tenga aplanamiento de flujo como Cw ). Yo agregaría que a veces el proceso de enumeración es costoso por otras razones, y por lo tanto debería usar una lista si tiene la intención de iterar sobre el IEnumerable más de una vez.

Por ejemplo, LINQ-to-objects se basa en "yield return". Si ha escrito una consulta LINQ lenta (por ejemplo, que filtra una lista grande en una lista pequeña, o que clasifica y agrupa), puede ser conveniente llamar a ToList() sobre el resultado de la consulta para evitar enumerar múltiples veces (que en realidad ejecuta la consulta varias veces).

Si elige entre "rentabilidad de rendimiento" y List<T> al escribir un método, considere: ¿es esto costoso y la persona que llama deberá enumerar los resultados más de una vez? Si sabe que la respuesta es sí, entonces no use "rentabilidad de rendimiento" a menos que la lista producida sea extremadamente grande (y no puede permitirse la memoria que usaría; recuerde, otro beneficio del yield es que la lista de resultados no lo hace). t tiene que estar completamente en la memoria a la vez).

Otra razón para no usar "rendimiento de retorno" es si las operaciones de entrelazado son peligrosas. Por ejemplo, si su método se ve así,

IEnumerable<T> GetMyStuff() { foreach (var x in MyCollection) if (...) yield return (...); }

esto es peligroso si existe la posibilidad de que MyCollection cambie debido a algo que hace la persona que llama:

foreach(T x in GetMyStuff()) { if (...) MyCollection.Add(...); // Oops, now GetMyStuff() will throw an exception because // MyCollection was modified. }

yield return puede causar problemas cada vez que el que llama cambia algo que la función de rendimiento asume no cambia.


Evitaría usar el yield return si el método tiene un efecto secundario que espera al llamar al método. Esto se debe a la ejecución diferida que menciona Pop Catalin .

Un efecto secundario podría ser modificar el sistema, lo que podría ocurrir en un método como IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos() , que rompe el principio de responsabilidad única . Eso es bastante obvio (ahora ...), pero un efecto secundario no tan obvio podría ser establecer un resultado en caché o similar como una optimización.

Mis reglas generales (nuevamente, ahora ...) son:

  • Solo use yield si el objeto devuelto requiere un poco de procesamiento
  • No hay efectos secundarios en el método si necesito usar el yield
  • Si tiene que tener efectos secundarios (y limitar eso al almacenamiento en caché, etc.), no use el yield y asegúrese de que los beneficios de expandir la iteración superen los costos

La clave para darse cuenta es para qué es útil el yield , entonces usted puede decidir qué casos no se benefician.

En otras palabras, cuando no necesita una secuencia para ser evaluado perezosamente, puede omitir el uso del yield . ¿Cuándo sería eso? Sería cuando no te importa tener inmediatamente toda tu colección en la memoria. De lo contrario, si tiene una gran secuencia que afectaría negativamente a la memoria, querría usar el yield para trabajar en él paso a paso (es decir, de forma perezosa). Un generador de perfiles puede ser útil al comparar ambos enfoques.

Observe cómo la mayoría de las declaraciones LINQ devuelven un IEnumerable<T> . Esto nos permite enrutar continuamente diferentes operaciones LINQ juntas sin afectar negativamente el rendimiento en cada paso (también conocido como ejecución diferida). La imagen alternativa estaría poniendo una ToList() a ToList() entre cada declaración LINQ. Esto provocaría que cada sentencia LINQ anterior se ejecutara inmediatamente antes de realizar la siguiente instrucción LINQ (encadenada), renunciando así a cualquier beneficio de la evaluación diferida y utilizando IEnumerable<T> hasta que sea necesario.


Si está definiendo un método de extensión Linq-y en el que está envolviendo a los miembros actuales de Linq, esos miembros a menudo devolverán un iterador. Ceder a través de ese iterador usted mismo es innecesario.

Más allá de eso, no se puede meter en problemas utilizando el rendimiento para definir un enumerable de "transmisión" que se evalúa en base a JAT.


Tengo que mantener un montón de código de un tipo que estaba absolutamente obsesionado con el retorno de rendimiento e IEnumerable. El problema es que muchas de las API de terceros que utilizamos, así como muchos de nuestros propios códigos, dependen de listas o matrices. Así que termino teniendo que hacer:

IEnumerable<foo> myFoos = getSomeFoos(); List<foo> fooList = new List<foo>(myFoos); thirdPartyApi.DoStuffWithArray(fooList.ToArray());

No necesariamente es malo, pero es un poco molesto de tratar, y en algunas ocasiones lleva a la creación de Listas duplicadas en la memoria para evitar la refacturación de todo.


Uno que podría atraparlo es si está serializando los resultados de una enumeración y enviándolos a través del cable. Debido a que la ejecución se difiere hasta que se necesiten los resultados, serializará una enumeración vacía y la enviará de regreso en lugar de los resultados que desea.