¿El “foreach” causa la ejecución repetida de Linq?

He estado trabajando por primera vez con Entity Framework en .NET y he estado escribiendo consultas LINQ para obtener información de mi modelo. Me gustaría progtwigr buenos hábitos desde el principio, así que he estado investigando sobre la mejor manera de escribir estas consultas y obtener sus resultados. Desafortunadamente, al explorar Stack Exchange, parece que he encontrado dos explicaciones contradictorias sobre cómo funciona la ejecución diferida / inmediata con LINQ:

  • Un foreach hace que la consulta se ejecute en cada iteración del ciclo:

Demostrado en cuestión Slow foreach () en una consulta LINQ – ToList () aumenta el rendimiento inmensamente – ¿por qué es esto? , la implicación es que se debe llamar a “ToList ()” para evaluar la consulta de inmediato, ya que el foreach está evaluando la consulta en el origen de datos repetidamente, ralentizando la operación considerablemente.

Otro ejemplo es la pregunta de Availing a través de resultados agrupados de linq es increíblemente lento, ¿algún consejo? , donde la respuesta aceptada también implica que llamar a “ToList ()” en la consulta mejorará el rendimiento.

  • Un foreach hace que una consulta se ejecute una vez, y es seguro de usar con LINQ

Demostrado en cuestión ¿Foreach ejecuta la consulta solo una vez? , la implicación es que foreach hace que se establezca una enumeración y no consultará el origen de datos cada vez.

La exploración continua del sitio ha arrojado muchas preguntas donde “ejecución repetida durante un ciclo foreach” es el culpable de la preocupación por el rendimiento, y muchas otras respuestas que afirman que un foreach apropiadamente obtendrá una sola consulta de un origen de datos, lo que significa que ambos las explicaciones parecen tener validez. Si la hipótesis “ToList ()” es incorrecta (como parece suponer la mayoría de las respuestas actuales a partir del 2013-06-05 1:51 PM EST), ¿de dónde proviene este concepto erróneo? ¿Hay alguna de estas explicaciones que sea precisa y otra que no lo sea, o existen circunstancias diferentes que podrían causar que una consulta LINQ se evalúe de manera diferente?

Editar: Además de la respuesta aceptada a continuación, he presentado la siguiente pregunta sobre progtwigdores que me ayudó mucho a comprender la ejecución de consultas, particularmente las trampas que podrían dar como resultado múltiples aciertos de fonts de datos durante un ciclo, que creo que ser útil para otros interesados ​​en esta pregunta: https://softwareengineering.stackexchange.com/questions/178218/for-vs-foreach-vs-linq

En general, LINQ utiliza la ejecución diferida. Si utiliza métodos como First() y FirstOrDefault() la consulta se ejecuta inmediatamente. Cuando haces algo como;

 foreach(string s in MyObjects.Select(x => x.AStringProp)) 

Los resultados se recuperan de forma continua, es decir, uno por uno. Cada vez que el iterador llama a MoveNext la proyección se aplica al siguiente objeto. Si fuera a tener un Where debería aplicar primero el filtro, entonces la proyección.

Si haces algo como;

 List names = People.Select(x => x.Name).ToList(); foreach (string name in names) 

Entonces creo que esta es una operación inútil. ToList() obligará a la consulta a ejecutarse, enumerando la lista People y aplicando la proyección x => x.Name . Luego enumerará la lista nuevamente. Entonces, a menos que tenga una buena razón para tener los datos en una lista (en lugar de IEnumerale), está perdiendo ciclos de CPU.

En general, usar una consulta LINQ en la colección que está enumerando con un foreach no tendrá un rendimiento peor que cualquier otra opción similar y práctica.

También vale la pena señalar que se alienta a las personas que implementan proveedores de LINQ a hacer que los métodos comunes funcionen como lo hacen en los proveedores proporcionados por Microsoft, pero no están obligados a hacerlo. Si fuera a escribir un LINQ a HTML o LINQ a mi proveedor de formato de datos propietarios, no habría garantía de que se comporte de esta manera. Tal vez la naturaleza de los datos haría de la ejecución inmediata la única opción práctica.

Además, edición final; si estás interesado en esto, C # In Depth de Jon Skeet es muy informativo y de gran lectura. Mi respuesta resume algunas páginas del libro (con suerte, con una precisión razonable), pero si desea obtener más detalles sobre cómo funciona LINQ bajo las sábanas, es un buen lugar para buscar.

prueba esto con LinqPad

 void Main() { var testList = Enumerable.Range(1,10); var query = testList.Where(x => { Console.WriteLine(string.Format("Doing where on {0}", x)); return x % 2 == 0; }); Console.WriteLine("First foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0}", i)); } Console.WriteLine("First foreach ending"); Console.WriteLine("Second foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i)); } Console.WriteLine("Second foreach ending"); } 

Cada vez que se ejecuta el delegado where, veremos una salida de consola, por lo tanto, podemos ver que la consulta de Linq se ejecuta cada vez. Ahora, al observar la salida de la consola, vemos que el segundo bucle foreach aún hace que “Doing where on” se imprima, lo que demuestra que el segundo uso de foreach hace que la cláusula where se ejecute de nuevo … lo que puede causar una desaceleración .

 First foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 Doing where on 3 Doing where on 4 Foreached where on 4 Doing where on 5 Doing where on 6 Foreached where on 6 Doing where on 7 Doing where on 8 Foreached where on 8 Doing where on 9 Doing where on 10 Foreached where on 10 First foreach ending Second foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 for the second time. Doing where on 3 Doing where on 4 Foreached where on 4 for the second time. Doing where on 5 Doing where on 6 Foreached where on 6 for the second time. Doing where on 7 Doing where on 8 Foreached where on 8 for the second time. Doing where on 9 Doing where on 10 Foreached where on 10 for the second time. Second foreach ending 

Depende de cómo se use la consulta Linq.

 var q = {some linq query here} while (true) { foreach(var item in q) { ... } } 

El código anterior ejecutará la consulta Linq varias veces. No por el foreach, sino porque el foreach está dentro de otro ciclo, por lo que el propio foreach se está ejecutando varias veces.

Si todos los consumidores de una consulta de linq lo usan “con cuidado” y evitan errores estúpidos como los bucles nesteds de arriba, entonces una consulta de linq no se debe ejecutar muchas veces innecesariamente.

Hay ocasiones en que la reducción de una consulta de linq a un conjunto de resultados en memoria usando ToList () está garantizada, pero en mi opinión, ToList () se usa mucho, con demasiada frecuencia. ToList () casi siempre se convierte en una píldora venenosa cuando se trata de datos grandes, ya que obliga a que todo el conjunto de resultados (potencialmente millones de filas) se guarde en la memoria, incluso si el consumidor / enumerador más externo solo necesita 10 filas. Avoid ToList () a menos que tenga una justificación muy específica y sepa que sus datos nunca serán grandes.

A veces puede ser una buena idea “almacenar en caché” una consulta LINQ utilizando ToList() o ToArray() , si se accede a la consulta varias veces en su código.

Pero tenga en cuenta que “almacenar en caché” sigue llamando foreach por turno.

Entonces la regla básica para mí es:

  • si una consulta simplemente se usa en un foreach (y eso es todo), entonces no guardo en caché la consulta
  • si se utiliza una consulta en un foreach y en algunos otros lugares del código, entonces lo ToList/ToArray en una var usando ToList/ToArray

foreach , por sí solo, solo ejecuta sus datos una vez. De hecho, específicamente lo atraviesa una vez. No puede mirar hacia delante o hacia atrás, ni alterar el índice de la forma en que lo hace con un bucle for .

Sin embargo, si tiene múltiples foreach en su código, todos operando en la misma consulta LINQ, puede hacer que la consulta se ejecute varias veces. Sin embargo, esto es completamente dependiente de los datos . Si está iterando sobre un IQueryable / IEnumerable basado en IEnumerable que representa una consulta de base de datos, ejecutará esa consulta cada vez. Si está iterando sobre una List u otra colección de objetos, se ejecutará a través de la lista cada vez, pero no golpeará su base de datos varias veces.

En otras palabras, esta es una propiedad de LINQ , no una propiedad de foreach .

La diferencia está en el tipo subyacente. Como LINQ está construido sobre IEnumerable (o IQueryable), el mismo operador LINQ puede tener características de rendimiento completamente diferentes.

Una lista siempre será rápida para responder, pero se necesita un esfuerzo inicial para crear una lista.

Un iterador también es IEnumerable y puede emplear cualquier algoritmo cada vez que obtenga el elemento “siguiente”. Esto será más rápido si no necesita pasar por el conjunto completo de elementos.

Puede convertir cualquier IEnumerable en una lista llamando a ToList () y almacenando la lista resultante en una variable local. Esto es recomendable si

  • Usted no depende de la ejecución diferida.
  • Tienes que acceder a más elementos totales que todo el conjunto.
  • Puede pagar el costo inicial de recuperar y almacenar todos los artículos.

Usando LINQ, incluso sin entidades, lo que obtendrás es que la ejecución diferida está en vigencia. Solo forzando una iteración se evalúa la expresión de linq real. En ese sentido, cada vez que uses la expresión linq se evaluará.

Ahora, con las entidades, esto sigue siendo lo mismo, pero aquí solo funciona más funcionalidad. Cuando el marco de la entidad ve la expresión por primera vez, mira si ya ha ejecutado esta consulta. De lo contrario, irá a la base de datos y buscará los datos, configurará su modelo de memoria interna y le devolverá los datos. Si el marco de trabajo de la entidad considera que ya ha obtenido los datos de antemano, no irá a la base de datos y utilizará el modelo de memoria que configuró antes para devolverle los datos.

Esto puede hacer su vida más fácil, pero también puede ser un dolor. Por ejemplo, si solicita todos los registros de una tabla utilizando una expresión linq. El marco de la entidad cargará todos los datos de la tabla. Si más tarde evalúa la misma expresión de linq, incluso si en el momento en que se borraron o agregaron los registros, obtendrá el mismo resultado.

El marco de la entidad es algo complicado. Por supuesto, hay formas de hacer que vuelva a ejecutar la consulta, teniendo en cuenta los cambios que tiene en su propio modelo de memoria y similares.

Sugiero leer “framework de entidad de progtwigción” de Julia Lerman. Aborda muchos problemas como el que tienes ahora.

Ejecutará la instrucción LINQ el mismo número de veces, sin importar si lo hace .ToList() o no. Tengo un ejemplo aquí con salida de color a la consola:

Qué sucede en el código (ver código en la parte inferior):

  • Crea una lista de 100 ints (0-99).
  • Cree una instrucción LINQ que imprima cada int de la lista seguido de dos * a la consola en color rojo, y luego devuelva el int si es un número par.
  • Haga un foreach en la query , imprimiendo cada número par en color verde.
  • Haga un foreach en la query.ToList() , imprimiendo cada número par en color verde.

Como puede ver en el siguiente resultado, el número de entradas escritas en la consola es el mismo, lo que significa que la instrucción LINQ se ejecuta la misma cantidad de veces.

La diferencia está en cuando se ejecuta la statement . Como puede ver, cuando hace un foreach en la consulta (que no ha invocado .ToList() en), la lista y el objeto IEnumerable, devuelto de la instrucción LINQ, se enumeran al mismo tiempo.

Cuando primero almacena en la memoria caché, se enumeran por separado, pero igual la cantidad de veces.

La diferencia es muy importante de entender , porque si la lista se modifica después de haber definido su statement LINQ, la instrucción LINQ operará en la lista modificada cuando se ejecute (por ejemplo, por .ToList() ). PERO si fuerza la ejecución de la statement LINQ ( .ToList() ) y luego modifica la lista posteriormente, la instrucción LINQ NO funcionará en la lista modificada.

Aquí está el resultado: Salida de Ejecución Diferida LINQ

Aquí está mi código:

 // Main method: static void Main(string[] args) { IEnumerable ints = Enumerable.Range(0, 100); var query = ints.Where(x => { Console.ForegroundColor = ConsoleColor.Red; Console.Write($"{x}**, "); return x % 2 == 0; }); DoForeach(query, "query"); DoForeach(query, "query.ToList()"); Console.ForegroundColor = ConsoleColor.White; } // DoForeach method: private static void DoForeach(IEnumerable collection, string collectionName) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH BEGIN: ---", collectionName); if (collectionName.Contains("query.ToList()")) collection = collection.ToList(); foreach (var item in collection) { Console.ForegroundColor = ConsoleColor.Green; Console.Write($"{item}, "); } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH END ---", collectionName); } 

Nota sobre el tiempo de ejecución: hice algunas pruebas de tiempo (aunque no lo suficiente para publicarlo aquí) y no encontré ninguna consistencia en ninguno de los métodos que sea más rápido que el otro (incluida la ejecución de .ToList() en el tiempo). En colecciones más grandes, almacenar en caché primero la colección y luego iterar parecía un poco más rápido, pero no hubo una conclusión definitiva de mi prueba.