Cuando escribo código en Visual Studio, ReSharper (¡Dios lo bendiga!) A menudo me sugiere que cambie mi bucle for de la vieja escuela en una forma foreach más compacta.
Y a menudo, cuando acepto este cambio, ReSharper da un paso adelante y me sugiere que lo cambie nuevamente, en una forma brillante de LINQ.
Entonces, me pregunto: ¿hay algunas ventajas reales en estas mejoras? En una ejecución de código bastante simple, no puedo ver ningún aumento de velocidad (obviamente), pero puedo ver que el código se vuelve cada vez menos legible ... Entonces me pregunto: ¿vale la pena?
foreach
es eliminar elementos de una colección mientras se enumera, donde generalmentefor
se necesita un bucle para comenzar desde el último elemento.Respuestas:
for
vs.foreach
Existe una confusión común de que esas dos construcciones son muy similares y que ambas son intercambiables de esta manera:
y:
El hecho de que ambas palabras clave comiencen por las mismas tres letras no significa que, semánticamente, sean similares. Esta confusión es extremadamente propensa a errores, especialmente para principiantes. Iterar a través de una colección y hacer algo con los elementos se hace con
foreach
;for
no tiene que y no debe usarse para este propósito , a menos que realmente sepa lo que está haciendo.Veamos qué le pasa con un ejemplo. Al final, encontrará el código completo de una aplicación de demostración utilizada para recopilar los resultados.
En el ejemplo, estamos cargando algunos datos de la base de datos, más precisamente las ciudades de Adventure Works, ordenadas por nombre, antes de encontrarnos con "Boston". Se utiliza la siguiente consulta SQL:
Los datos se cargan por el
ListCities()
método que devuelve unIEnumerable<string>
. Así es como seforeach
ve:Reescribámoslo con a
for
, suponiendo que ambos sean intercambiables:Ambos devuelven las mismas ciudades, pero hay una gran diferencia.
foreach
,ListCities()
se llama una vez y produce 47 artículos.for
,ListCities()
se llama 94 veces y produce 28153 elementos en general.¿Que pasó?
IEnumerable
es perezosa . Significa que hará el trabajo solo en el momento en que se necesite el resultado. La evaluación diferida es un concepto muy útil, pero tiene algunas advertencias, incluido el hecho de que es fácil pasar por alto los momentos en los que se necesitará el resultado, especialmente en los casos en que el resultado se usa varias veces.En el caso de a
foreach
, el resultado se solicita solo una vez. En el caso de unfor
implementado en el código escrito incorrectamente arriba , el resultado se solicita 94 veces , es decir, 47 × 2:Cada vez que
cities.Count()
se llama (47 veces),Cada vez
cities.ElementAt(i)
se llama (47 veces).Consultar una base de datos 94 veces en lugar de una es terrible, pero no es lo peor que puede suceder. Imagine, por ejemplo, lo que sucedería si la
select
consulta fuera precedida por una consulta que también inserte una fila en la tabla. Bien, tendríamosfor
que llamará a la base de datos 2,147,483,647 veces, a menos que esperemos que se bloquee antes.Por supuesto, mi código está sesgado. Deliberadamente utilicé la pereza
IEnumerable
y la escribí para llamar repetidamenteListCities()
. Uno puede notar que un principiante nunca hará eso, porque:El
IEnumerable<T>
no tiene la propiedadCount
, sino solo el métodoCount()
. Llamar a un método es aterrador, y uno puede esperar que su resultado no se almacene en caché y no sea adecuado en unfor (; ...; )
bloque.La indexación no está disponible
IEnumerable<T>
y no es obvio encontrar elElementAt
método de extensión LINQ.Probablemente la mayoría de los principiantes simplemente convertirían el resultado de
ListCities()
algo con lo que estén familiarizados, como aList<T>
.Aún así, este código es muy diferente de la
foreach
alternativa. Nuevamente, da los mismos resultados, y esta vez elListCities()
método se llama solo una vez, pero produce 575 ítems, mientras que conforeach
solo rindió 47 ítems.La diferencia viene del hecho de que
ToList()
hace todos los datos que se cargan desde la base de datos. Si bien seforeach
solicitan solo las ciudades antes de "Boston", la nuevafor
exige que todas las ciudades sean recuperadas y almacenadas en la memoria. Con 575 cadenas cortas, probablemente no haga mucha diferencia, pero ¿qué pasaría si estuviéramos recuperando solo unas pocas filas de una tabla que contiene miles de millones de registros?Entonces
foreach
, ¿qué es realmente?foreach
está más cerca de un bucle while. El código que usé anteriormente:puede ser simplemente reemplazado por:
Ambos producen la misma IL. Ambos tienen el mismo resultado. Ambos tienen los mismos efectos secundarios. Por supuesto, esto
while
puede reescribirse en un infinito similarfor
, pero sería aún más largo y propenso a errores. Usted es libre de elegir el que le resulte más legible.¿Quieres probarlo tú mismo? Aquí está el código completo:
Y los resultados:
LINQ vs. forma tradicional
En cuanto a LINQ, es posible que desee aprender programación funcional (FP), no cosas de C # FP, sino un lenguaje FP real como Haskell. Los lenguajes funcionales tienen una forma específica de expresar y presentar el código. En algunas situaciones, es superior a los paradigmas no funcionales.
Se sabe que FP es muy superior cuando se trata de manipular listas ( lista como un término genérico, no relacionado
List<T>
). Dado este hecho, la capacidad de expresar el código C # de una manera más funcional cuando se trata de listas es algo bastante bueno.Si no está convencido, compare la legibilidad del código escrito de manera funcional y no funcional en mi respuesta anterior sobre el tema.
fuente
foreach
es más eficiente quefor
, cuando en realidad la disparidad es el resultado de un código deliberadamente roto. La minuciosidad de la respuesta se redime, pero es fácil ver cómo un observador casual puede llegar a conclusiones erróneas.Si bien hay algunas grandes exposiciones ya sobre las diferencias entre for y foreach. Hay algunas tergiversaciones groseras sobre el papel de LINQ.
La sintaxis LINQ no es solo un azúcar sintáctico que proporciona una aproximación de programación funcional a C #. LINQ proporciona construcciones funcionales que incluyen todos los beneficios de las mismas para C #. Combinado con la devolución de IEnumerable en lugar de IList, LINQ proporciona la ejecución diferida de la iteración. Lo que la gente suele hacer ahora es construir y devolver una IList de sus funciones de esta manera
En su lugar, use la sintaxis de retorno de rendimiento para crear una enumeración diferida.
Ahora la enumeración no se producirá hasta que haga una Lista o itere sobre ella. Y solo ocurre según sea necesario (aquí hay una enumeración de Fibbonaci que no tiene un problema de desbordamiento de pila)
Realizar un foreach sobre la función Fibonacci devolverá la secuencia de 46. Si quieres el 30, eso es todo lo que se calculará
Donde podemos divertirnos mucho es el soporte en el lenguaje para las expresiones lambda (combinado con las construcciones IQueryable e IQueryProvider, esto permite la composición funcional de consultas en una variedad de conjuntos de datos, el IQueryProvider es responsable de interpretar el pasado expresiones y crear y ejecutar una consulta usando las construcciones nativas de la fuente). No voy a entrar en detalles minuciosos aquí, pero hay una serie de publicaciones de blog que muestran cómo crear un proveedor de consultas SQL aquí.
En resumen, debería preferir devolver IEnumerable sobre IList cuando los consumidores de su función realicen una iteración simple. Y use las capacidades de LINQ para diferir la ejecución de consultas complejas hasta que sean necesarias.
fuente
La legibilidad está en el ojo del espectador. Algunas personas pueden decir
es perfectamente legible; otros podrían decir que esto es opaco y preferirían
como aclarar lo que se está haciendo. No podemos decirle qué le parece más legible. Pero es posible que pueda detectar algunos de mis propios prejuicios en el ejemplo que he construido aquí ...
fuente
La diferencia entre LINQ y
foreach
realmente se reduce a dos estilos de programación diferentes: imperativo y declarativo.Imperativo: en este estilo le dices a la computadora "haz esto ... ahora haz esto ... ahora haz esto ahora haz esto". Lo alimentas un programa paso a paso.
Declarativo: en este estilo, le dice a la computadora cuál quiere que sea el resultado y deja que descubra cómo llegar allí.
Un ejemplo clásico de estos dos estilos es comparar el código de ensamblaje (o C) con SQL. En la asamblea das instrucciones (literalmente) una a la vez. En SQL, usted expresa cómo unir datos y qué resultado desea de esos datos.
Un buen efecto secundario de la programación declarativa es que tiende a ser un poco más alto. Esto permite que la plataforma evolucione debajo de usted sin que tenga que cambiar su código. Por ejemplo:
¿Que está sucediendo aquí? ¿Distinct usa un núcleo? ¿Dos? ¿Cincuenta? No lo sabemos y no nos importa. Los desarrolladores de .NET podrían reescribirlo en cualquier momento, siempre que continúe realizando el mismo propósito, nuestro código podría mágicamente ser más rápido después de una actualización de código.
Este es el poder de la programación funcional. Y la razón por la que encontrará ese código en lenguajes como Clojure, F # y C # (escrito con una mentalidad de programación funcional) a menudo es 3x-10x más pequeño que sus contrapartes imperativas.
Finalmente, me gusta el estilo declarativo porque en C # la mayoría de las veces esto me permite escribir código que no mute los datos. En el ejemplo anterior,
Distinct()
no cambia la barra, devuelve una nueva copia de los datos. Esto significa que cualquiera que sea la barra, y de donde sea que venga, no cambió de repente.Entonces, como dicen los otros carteles, aprenda programación funcional. Cambiará tu vida. Y si puede, hágalo en un verdadero lenguaje de programación funcional. Prefiero Clojure, pero F # y Haskell también son excelentes opciones.
fuente
var foo = bar.Distinct()
es esencialmente unIEnumerator<T>
hasta que llame.ToList()
o.ToArray()
. Esa es una distinción importante porque si no eres consciente de eso, puede provocar errores difíciles de entender.¿Pueden otros desarrolladores del equipo leer LINQ?
Si no es así, no lo use o sucederá una de dos cosas:
A para cada bucle es perfecto para recorrer una lista, pero si eso no es lo que debe hacer, no use uno.
fuente