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
; 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:
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 foreach
ve:
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ó?
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 un for
implementado en el código escrito incorrectamente arriba , el resultado se solicita 94 veces , es decir, 47 × 2:
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íamos for
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 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 ElementAt
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 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 foreach
alternativa. Nuevamente, da los mismos resultados, y esta vez el ListCities()
método se llama solo una vez, pero produce 575 ítems, mientras que con foreach
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 se foreach
solicitan solo las ciudades antes de "Boston", la nueva for
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:
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 while
puede 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.