unity - yield return c# stack overflow
Uso adecuado de ''rendimiento'' (16)
La palabra clave de yield es una de esas keywords en C # que continúa desconcertándome, y nunca he estado seguro de que la esté usando correctamente.
De los siguientes dos códigos, ¿cuál es el preferido y por qué?
Versión 1: Usando retorno de rendimiento
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
foreach (Product product in products)
{
yield return product;
}
}
}
Versión 2: Devolver la lista
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
return products.ToList<Product>();
}
}
¿Y qué hay de esto?
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
return products.ToList();
}
}
Supongo que esto es mucho más limpio. Sin embargo, no tengo VS2008 a mano para verificar. En cualquier caso, si Productos implementa IEnumerable (como parece - se usa en una declaración foreach), lo devolvería directamente.
Como ejemplo conceptual para comprender cuándo debe usar el yield
, digamos que el método ConsumeLoop()
procesa los elementos devueltos / producidos por ProduceList()
:
void ConsumeLoop() {
foreach (Consumable item in ProduceList()) // might have to wait here
item.Consume();
}
IEnumerable<Consumable> ProduceList() {
while (KeepProducing())
yield return ProduceExpensiveConsumable(); // expensive
}
Sin yield
, la llamada a ProduceList()
puede tardar mucho tiempo porque debe completar la lista antes de regresar:
//pseudo-assembly
Produce consumable[0] // expensive operation, e.g. disk I/O
Produce consumable[1] // waiting...
Produce consumable[2] // waiting...
Produce consumable[3] // completed the consumable list
Consume consumable[0] // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]
Usando el yield
, se vuelve a organizar, de alguna manera funciona "en paralelo":
//pseudo-assembly
Produce consumable[0]
Consume consumable[0] // immediately Consume
Produce consumable[1]
Consume consumable[1] // consume next
Produce consumable[2]
Consume consumable[2] // consume next
Produce consumable[3]
Consume consumable[3] // consume next
Y, por último, como muchos ya han sugerido, debe usar la Versión 2 porque de todos modos ya tiene la lista completa.
Completar una lista temporal es como descargar todo el video, mientras que usar el yield
es como transmitir ese video.
Devuelve la lista directamente. Beneficios:
- Es mas claro
-
La lista es reutilizable.(el iterador no lo es) enrealidad no es cierto, gracias Jon
Debe usar el iterador (rendimiento) cuando crea que probablemente no tendrá que recorrer todo el camino hasta el final de la lista, o cuando no tenga fin. Por ejemplo, la llamada del cliente buscará el primer producto que satisfaga algún predicado, podría considerar usar el iterador, aunque ese es un ejemplo artificial, y probablemente haya mejores formas de lograrlo. Básicamente, si sabe de antemano que será necesario calcular toda la lista, simplemente hágalo por adelantado. Si crees que no lo hará, entonces considera usar la versión de iterador.
El rendimiento puede ser muy poderoso para los algoritmos en los que necesita recorrer millones de objetos. Considere el siguiente ejemplo en el que necesita calcular posibles viajes para el viaje compartido. Primero generamos posibles viajes:
static IEnumerable<Trip> CreatePossibleTrips()
{
for (int i = 0; i < 1000000; i++)
{
yield return new Trip
{
Id = i.ToString(),
Driver = new Driver { Id = i.ToString() }
};
}
}
Luego itera a través de cada viaje:
static void Main(string[] args)
{
foreach (var trip in CreatePossibleTrips(trips))
{
// possible trip is actually calculated only at this point, because of yield
if (IsTripGood(trip))
{
// match good trip
}
}
}
Si usa Lista en lugar de rendimiento, necesitará asignar 1 millón de objetos a la memoria (~ 190 mb) y este simple ejemplo tomará ~ 1400 ms para ejecutarse. Sin embargo, si usa el rendimiento, no necesita poner todos estos objetos temporales en la memoria y obtendrá una velocidad de algoritmo significativamente más rápida: este ejemplo tardará solo ~ 400 ms en ejecutarse sin ningún consumo de memoria.
El rendimiento tiene dos grandes usos.
Ayuda a proporcionar una iteración personalizada sin crear colecciones temporales. (cargando todos los datos y bucles)
Ayuda a hacer iteración con estado. ( transmisión)
A continuación se muestra un video simple que he creado con una demostración completa para apoyar los dos puntos anteriores.
El uso del rendimiento es similar al retorno de la palabra clave, excepto que devolverá un generator . Y el objeto generador solo atravesará una vez .
El rendimiento tiene dos beneficios:
- No necesita leer estos valores dos veces;
- Puede obtener muchos nodos secundarios pero no tiene que guardarlos todos en la memoria.
Hay otra explanation clara tal vez te ayude.
Este es un punto aparte del punto, pero como la pregunta está etiquetada como mejores prácticas, seguiré adelante y le agregaré mis dos centavos. Para este tipo de cosas, prefiero convertirlo en una propiedad:
public static IEnumerable<Product> AllProducts
{
get {
using (AdventureWorksEntities db = new AdventureWorksEntities()) {
var products = from product in db.Product
select product;
return products;
}
}
}
Claro, es un poco más de caldera, pero el código que usa esto se verá mucho más limpio:
prices = Whatever.AllProducts.Select (product => product.price);
vs
prices = Whatever.GetAllProducts().Select (product => product.price);
Nota: No haría esto por ningún método que pueda tardar un tiempo en hacer su trabajo.
Esto es lo que Chris Sells cuenta sobre esas declaraciones en The C # Programming Language ;
A veces olvido que el rendimiento no es lo mismo que el rendimiento, ya que el código después de un rendimiento puede ejecutarse. Por ejemplo, el código después de la primera vuelta aquí nunca se puede ejecutar:
int F() { return 1; return 2; // Can never be executed }
En contraste, el código después de la primera devolución de rendimiento aquí se puede ejecutar:
IEnumerable<int> F() { yield return 1; yield return 2; // Can be executed }
Esto a menudo me muerde en una declaración if:
IEnumerable<int> F() { if(...) { yield return 1; } // I mean this to be the only // thing returned yield return 2; // Oops! }
En estos casos, recordar que la rentabilidad del rendimiento no es "final", como la devolución es útil.
Esto va a parecer una sugerencia extraña, pero aprendí a usar la palabra clave de yield
en C # leyendo una presentación sobre generadores en Python: http://www.dabeaz.com/generators/Generators.pdf David M. Beazley. No necesitas saber mucho de Python para entender la presentación, no lo hice. Lo encontré muy útil para explicar no solo cómo funcionan los generadores, sino también por qué debería importarle.
Habría usado la versión 2 del código en este caso. Dado que tiene la lista completa de productos disponibles y eso es lo que espera el "consumidor" de esta llamada de método, sería necesario enviar la información completa a la persona que llama.
Si la persona que llama de este método requiere "una" información a la vez y el consumo de la siguiente información es a pedido, entonces sería beneficioso utilizar el rendimiento de rendimiento, lo que garantizará que el comando de ejecución se devuelva al que llama cuando Una unidad de información está disponible.
Algunos ejemplos en los que se podría usar el rendimiento es:
- Cálculo complejo, paso a paso, donde la persona que llama está esperando los datos de un paso a la vez
- Paginación en GUI: donde el usuario nunca puede llegar a la última página y solo se requiere revelar un subconjunto de información en la página actual
Para responder a sus preguntas, habría utilizado la versión 2.
La frase clave de rendimiento se usa para mantener la máquina de estado para una colección particular. Dondequiera que el CLR vea la frase clave de rendimiento que se está utilizando, CLR implementa un patrón de Enumerador para esa parte del código. Este tipo de implementación ayuda al desarrollador de todo el tipo de tuberías que tendríamos que hacer en ausencia de la palabra clave.
Supongamos que si el desarrollador está filtrando alguna colección, itere la colección y luego extraiga esos objetos en una nueva colección. Este tipo de fontanería es bastante monótono.
Más sobre la palabra clave aquí en este artículo .
Las dos piezas de código realmente están haciendo dos cosas diferentes. La primera versión atraerá a los miembros a medida que los necesite. La segunda versión cargará todos los resultados en la memoria antes de comenzar a hacer algo con ella.
No hay una respuesta correcta o incorrecta a esta. Cuál es preferible solo depende de la situación. Por ejemplo, si hay un límite de tiempo que tiene para completar su consulta y necesita hacer algo semi-complicado con los resultados, la segunda versión podría ser preferible. Pero tenga cuidado con los conjuntos de resultados grandes, especialmente si está ejecutando este código en modo de 32 bits. He sido mordido por las excepciones de OutOfMemory varias veces al hacer este método.
Sin embargo, la clave a tener en cuenta es lo siguiente: las diferencias están en la eficiencia. Por lo tanto, es probable que deba elegir el que simplifique el código y cambiarlo solo después del perfilado.
Sé que esta es una pregunta antigua, pero me gustaría ofrecer un ejemplo de cómo se puede usar creativamente la palabra clave de rendimiento. Realmente me he beneficiado de esta técnica. Esperemos que esto sea de ayuda para cualquier otra persona que tropiece con esta pregunta.
Nota: No piense en la palabra clave de rendimiento como simplemente otra forma de crear una colección. Una gran parte del poder del rendimiento viene del hecho de que la ejecución se detiene en su método o propiedad hasta que el código de llamada se repite en el siguiente valor. Aquí está mi ejemplo:
El uso de la palabra clave de rendimiento (junto con la implementación de Caliburn.Micro coroutines de Rob Eisenburg) me permite expresar una llamada asíncrona a un servicio web como este:
public IEnumerable<IResult> HandleButtonClick() {
yield return Show.Busy();
var loginCall = new LoginResult(wsClient, Username, Password);
yield return loginCall;
this.IsLoggedIn = loginCall.Success;
yield return Show.NotBusy();
}
Lo que esto hará es activar mi BusyIndicator, llamar al método de inicio de sesión en mi servicio web, establecer mi indicador IsLoggedIn en el valor de retorno y luego volver a desactivar el BusyIndicator.
Así es como funciona: IResult tiene un método de Ejecución y un evento Completado. Caliburn.Micro toma el IEnumerator de la llamada a HandleButtonClick () y lo pasa a un método Coroutine.BeginExecute. El método BeginExecute comienza a iterar a través de IResults. Cuando se devuelve el primer IResult, la ejecución se detiene dentro de HandleButtonClick (), y BeginExecute () adjunta un controlador de eventos al evento Completado y llama a Execute (). IResult.Execute () puede realizar una tarea síncrona o asíncrona y dispara el evento Completado cuando se realiza.
LoginResult se ve algo como esto:
public LoginResult : IResult {
// Constructor to set private members...
public void Execute(ActionExecutionContext context) {
wsClient.LoginCompleted += (sender, e) => {
this.Success = e.Result;
Completed(this, new ResultCompletionEventArgs());
};
wsClient.Login(username, password);
}
public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
public bool Success { get; private set; }
}
Puede ser útil configurar algo como esto y pasar por la ejecución para observar lo que está sucediendo.
¡Espero que esto ayude a alguien! Realmente he disfrutado explorando las diferentes formas en que se puede usar el rendimiento.
Suponiendo que la clase LINQ de sus productos utiliza un rendimiento similar para la enumeración / iteración, la primera versión es más eficiente porque solo genera un valor cada vez que se repite la iteración.
El segundo ejemplo es convertir el enumerador / iterador en una lista con el método ToList (). Esto significa que itera manualmente sobre todos los elementos en el enumerador y luego devuelve una lista plana.
Tiendo a usar rendimiento-rendimiento cuando calculo el siguiente artículo en la lista (o incluso el siguiente grupo de artículos).
Usando su Versión 2, debe tener la lista completa antes de regresar. Al utilizar rendimiento-retorno, solo necesita tener el siguiente artículo antes de regresar.
Entre otras cosas, esto ayuda a repartir el costo computacional de los cálculos complejos en un marco de tiempo mayor. Por ejemplo, si la lista está conectada a una GUI y el usuario nunca va a la última página, nunca se calculan los elementos finales de la lista.
Otro caso en el que es preferible la rentabilidad es si IEnumerable representa un conjunto infinito. Considere la lista de números primos, o una lista infinita de números aleatorios. Nunca puede devolver el IEnumerable completo a la vez, por lo que utiliza el rendimiento-retorno para devolver la lista de forma incremental.
En su ejemplo particular, tiene la lista completa de productos, así que usaría la versión 2.