c# - sintaxis - ¿El "foreach" causa la ejecución repetida de Linq?
linq introduction (7)
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 anidados 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, porque 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.
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 programar 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 de "ToList ()" es incorrecta (como parece suponer la mayoría de las respuestas actuales desde 2013-06-05 1:51 PM EST), ¿de dónde viene 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 forma diferente?
Editar: Además de la respuesta aceptada a continuación, he presentado la siguiente pregunta sobre programadores que me ayudó mucho a comprender la ejecución de consultas, particularmente las trampas que podrían dar como resultado múltiples aciertos de fuentes 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<string> 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 Propios, 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.
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 programación" de Julia Lerman. Aborda muchos problemas como el que tienes ahora.
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 .
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
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 loToList/ToArray
en una var usandoToList/ToArray