para vs. foreach vs. LINQ

86

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?

beccoblu
fuente
2
Solo una nota: la sintaxis LINQ es bastante legible si está familiarizado con la sintaxis SQL. También hay dos formatos para LINQ (las expresiones lambda similares a SQL y los métodos encadenados), que podrían facilitar el aprendizaje. Podrían ser solo las sugerencias de ReSharper las que hacen que parezca ilegible.
Shauna
3
Como regla general, generalmente uso foreach a menos que trabaje con una matriz de longitud conocida o casos similares donde el número de iteraciones es relevante. En cuanto a LINQ-ifying, generalmente veré qué hace ReSharper de un foreach, y si la declaración LINQ resultante es ordenada / trivial / legible, la uso, y de lo contrario la revierto. Si sería una tarea difícil volver a escribir la lógica original que no es de LINQ si los requisitos cambiaran o si fuera necesario depurar de forma granular a través de la lógica de la que se está abstrayendo la declaración LINQ, no la LINQ y la dejo por mucho tiempo formar.
Ed Hastings
Un error común con foreaches eliminar elementos de una colección mientras se enumera, donde generalmente forse necesita un bucle para comenzar desde el último elemento.
Slai
Puede tomar valor de Øredev 2013 - Jessica Kerr - Principios funcionales para desarrolladores orientados a objetos . Linq entra en la presentación poco después de los 33 minutos, bajo el título "Estilo declarativo".
Theraot

Respuestas:

139

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:

foreach (var c in collection)
{
    DoSomething(c);
}

y:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

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; forno 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:

select distinct [City] from [Person].[Address] order by [City]

Los datos se cargan por el ListCities()método que devuelve un IEnumerable<string>. Así es como se foreachve:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Reescribámoslo con a for, suponiendo que ambos sean intercambiables:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Ambos devuelven las mismas ciudades, pero hay una gran diferencia.

  • Cuando se usa foreach, ListCities()se llama una vez y produce 47 artículos.
  • Cuando se usa for, ListCities()se llama 94 veces y produce 28153 elementos en general.

¿Que pasó?

IEnumerablees 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 un for 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 selectconsulta fuera precedida por una consulta que también inserte una fila en la tabla. Bien, tendríamos forque 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 IEnumerabley la escribí para llamar repetidamente ListCities(). Uno puede notar que un principiante nunca hará eso, porque:

  • El IEnumerable<T>no tiene la propiedad Count, sino solo el método Count(). Llamar a un método es aterrador, y uno puede esperar que su resultado no se almacene en caché y no sea adecuado en un for (; ...; )bloque.

  • La indexación no está disponible IEnumerable<T>y no es obvio encontrar el ElementAtmé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 a List<T>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Aún así, este código es muy diferente de la foreachalternativa. Nuevamente, da los mismos resultados, y esta vez el ListCities()método se llama solo una vez, pero produce 575 ítems, mientras que con foreachsolo 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 se foreachsolicitan solo las ciudades antes de "Boston", la nueva forexige 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?

foreachestá más cerca de un bucle while. El código que usé anteriormente:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

puede ser simplemente reemplazado por:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

Ambos producen la misma IL. Ambos tienen el mismo resultado. Ambos tienen los mismos efectos secundarios. Por supuesto, esto whilepuede reescribirse en un infinito similar for, 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:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

Y los resultados:

--- para ---
Abingdon Albany Alexandria Alhambra [...] Bonn Burdeos Boston

Los datos se llamaron 94 veces y arrojaron 28153 artículos.

--- para con la lista ---
Abingdon Albany Alexandria Alhambra [...] Bonn Burdeos Boston

Los datos se llamaron 1 veces y arrojaron 575 artículos.

--- mientras ---
Abingdon Albany Alexandria Alhambra [...] Bonn Burdeos Boston

Los datos se denominaron 1 veces y arrojaron 47 artículos.

--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Burdeos Boston

Los datos se denominaron 1 veces y arrojaron 47 artículos.

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.

Arseni Mourzenko
fuente
1
Pregunta sobre el ejemplo ListCities (). ¿Por qué funcionaría solo una vez? No he tenido problemas para predecir sobre los rendimientos en el pasado.
Dante
1
No está diciendo que solo obtendría un resultado de IEnumerable; está diciendo que la consulta SQL (que es la parte costosa del método) solo se ejecutaría una vez, esto es algo bueno. Luego leería y produciría todos los resultados de la consulta.
HappyCat
99
@Giorgio: Si bien esta pregunta es comprensible, tener la semántica de un idioma satisface lo que un principiante puede encontrar confuso no nos dejaría con un lenguaje muy efectivo.
Steven Evers
44
LINQ no es solo azúcar semántico. Proporciona ejecución retrasada. Y en el caso de IQueryables (por ejemplo, Entity Framework) permite que la consulta se pase y se componga hasta que se repita (lo que significa que agregar una cláusula where a un IQueryable devuelto dará como resultado que el SQL pase al servidor tras la iteración para incluir esa cláusula where) descargando el filtrado en el servidor).
Michael Brown
8
Por mucho que me guste esta respuesta, creo que los ejemplos son un tanto artificiales. El resumen al final sugiere que foreaches más eficiente que for, 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.
Robert Harvey
19

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

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

En su lugar, use la sintaxis de retorno de rendimiento para crear una enumeración diferida.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

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)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

Realizar un foreach sobre la función Fibonacci devolverá la secuencia de 46. Si quieres el 30, eso es todo lo que se calculará

var thirtiethFib=Fibonacci().Skip(29).Take(1);

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.

Michael Brown
fuente
13

pero puedo ver que el código se vuelve cada vez menos legible

La legibilidad está en el ojo del espectador. Algunas personas pueden decir

var common = list1.Intersect(list2);

es perfectamente legible; otros podrían decir que esto es opaco y preferirían

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

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í ...

AakashM
fuente
28
Honestamente, diría que Linq hace que la intención sea objetivamente más legible, mientras que los bucles hacen que el mecanismo sea más legible objetivamente.
jk.
16
Corría lo más rápido que puedo de alguien que me dice que la versión for-for-if es más legible que la versión de intersección.
Konamiman
3
@Konamiman: eso dependería de lo que una persona busque cuando piense en la "legibilidad". El comentario de jk. ilustra esto perfectamente. El bucle es más legible en el sentido de que puede ver fácilmente cómo está obteniendo su resultado final, mientras que el LINQ es más legible en lo que debería ser el resultado final.
Shauna
2
Es por eso que el ciclo entra en la implementación, y luego usa Intersect en todas partes.
R. Martinho Fernandes
8
@Shauna: Imagine la versión for-loop dentro de un método haciendo varias otras cosas; es un desastre. Entonces, naturalmente, lo divide en su propio método. En términos de legibilidad, esto es lo mismo que IEnumerable <T> .Intersect, pero ahora ha duplicado la funcionalidad del marco e introducido más código para mantener. La única excusa es si necesita una implementación personalizada por razones de comportamiento, pero aquí solo estamos hablando de legibilidad.
Misko
7

La diferencia entre LINQ y foreachrealmente 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:

var foo = bar.Distinct();

¿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.

Timothy Baldridge
fuente
2
El procesamiento de LINQ se difiere hasta que realmente se itera sobre él. var foo = bar.Distinct()es esencialmente un IEnumerator<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.
Berin Loritsch
-5

¿Pueden otros desarrolladores del equipo leer LINQ?

Si no es así, no lo use o sucederá una de dos cosas:

  1. Tu código será imposible de mantener
  2. Tendrás que mantener todo tu código y todo lo que se base en él

A para cada bucle es perfecto para recorrer una lista, pero si eso no es lo que debe hacer, no use uno.

Llama invertida
fuente
11
hmm, agradezco que para un solo proyecto esta sea la respuesta, pero para el mediano y largo plazo deberías entrenar a tu personal, de lo contrario tienes una carrera hacia la comprensión del código que no suena como una buena idea.
jk.
21
En realidad, puede suceder una tercera cosa: los otros desarrolladores podrían hacer un pequeño esfuerzo y aprender algo nuevo y útil. No es inaudito.
Eric King
66
@InvertedLlama si estuviera en una empresa donde los desarrolladores necesitan capacitación formal para comprender nuevos conceptos de lenguaje, entonces estaría pensando en encontrar una nueva empresa.
Wyatt Barnett
13
Tal vez pueda salirse con la suya con las bibliotecas, pero cuando se trata de las características principales del lenguaje, eso no es suficiente. Puede elegir marcos de trabajo. Pero un buen programador .NET necesita comprender todas y cada una de las características del lenguaje y de la plataforma central (System. *). Y teniendo en cuenta que ni siquiera puedes usar EF correctamente sin usar Linq, debo decir que ... en la actualidad, si eres un programador de .NET y no conoces a Linq, eres incompetente.
Timothy Baldridge
77
Esto ya tiene suficientes votos negativos, así que no añadiré nada a eso, pero un argumento que respalde a compañeros de trabajo ignorantes / incompetentes nunca es válido.
Steven Evers