for
vs. foreach
Existe uma confusão comum de que essas duas construções são muito semelhantes e que ambas são intercambiáveis assim:
foreach (var c in collection)
{
DoSomething(c);
}
e:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
O fato de ambas as palavras-chave começarem pelas mesmas três letras não significa que, semanticamente, elas são semelhantes. Essa confusão é extremamente suscetível a erros, especialmente para iniciantes. Iterar através de uma coleção e fazer algo com os elementos é feito com foreach
; for
não precisa e não deve ser usado para esse fim , a menos que você realmente saiba o que está fazendo.
Vamos ver o que há de errado com um exemplo. No final, você encontrará o código completo de um aplicativo de demonstração usado para reunir os resultados.
No exemplo, estamos carregando alguns dados do banco de dados, mais precisamente as cidades da Adventure Works, ordenadas por nome, antes de encontrar "Boston". A seguinte consulta SQL é usada:
select distinct [City] from [Person].[Address] order by [City]
Os dados são carregados pelo ListCities()
método que retorna um IEnumerable<string>
. Aqui está o que foreach
parece:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Vamos reescrevê-lo com a for
, assumindo que ambos sejam intercambiáveis:
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 retornam as mesmas cidades, mas há uma enorme diferença.
- Ao usar
foreach
, ListCities()
é chamado uma vez e produz 47 itens.
- Ao usar
for
, ListCities()
é chamado 94 vezes e produz 28153 itens no total.
O que aconteceu?
IEnumerable
é preguiçoso . Isso significa que ele fará o trabalho somente no momento em que o resultado for necessário. A avaliação preguiçosa é um conceito muito útil, mas possui algumas ressalvas, incluindo o fato de que é fácil perder o (s) momento (s) em que o resultado será necessário, especialmente nos casos em que o resultado é usado várias vezes.
No caso de a foreach
, o resultado é solicitado apenas uma vez. No caso de a for
implementado no código incorretamente escrito acima , o resultado é solicitado 94 vezes , ou seja, 47 × 2:
Toda vez que cities.Count()
é chamado (47 vezes),
Toda vez que cities.ElementAt(i)
é chamado (47 vezes).
Consultar um banco de dados 94 vezes em vez de um é terrível, mas não é a pior coisa que pode acontecer. Imagine, por exemplo, o que aconteceria se a select
consulta fosse precedida por uma consulta que também insere uma linha na tabela. Certo, teríamos o for
que chamará o banco de dados 2.147.483.647 vezes, a menos que esperemos que ele trava antes.
Claro, meu código é tendencioso. Eu deliberadamente usei a preguiça IEnumerable
e a escrevi de forma a ligar repetidamente ListCities()
. Pode-se notar que um iniciante nunca fará isso, porque:
O IEnumerable<T>
não tem a propriedade Count
, mas apenas o método Count()
. Chamar um método é assustador e pode-se esperar que seu resultado não seja armazenado em cache e não seja adequado em um for (; ...; )
bloco.
A indexação está indisponível IEnumerable<T>
e não é óbvio encontrar o ElementAt
método de extensão LINQ.
Provavelmente, a maioria dos iniciantes converteria o resultado ListCities()
em algo que eles estão 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;
}
}
Ainda assim, esse código é muito diferente da foreach
alternativa. Novamente, ele fornece os mesmos resultados e, desta vez, o ListCities()
método é chamado apenas uma vez, mas gera 575 itens, enquanto que com foreach
ele produz apenas 47 itens.
A diferença vem do fato que ToList()
faz com que todos os dados a ser carregado a partir do banco de dados. Embora foreach
solicitado apenas nas cidades anteriores a "Boston", o novo for
exige que todas as cidades sejam recuperadas e armazenadas na memória. Com 575 strings curtas, provavelmente não faz muita diferença, mas e se estivéssemos recuperando apenas algumas linhas de uma tabela contendo bilhões de registros?
Então, o que é foreach
realmente?
foreach
está mais perto de um loop while. O código que eu usei anteriormente:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
pode ser simplesmente substituído por:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Ambos produzem a mesma IL. Ambos têm o mesmo resultado. Ambos têm os mesmos efeitos colaterais. Obviamente, isso while
pode ser reescrito em um infinito semelhante for
, mas seria ainda mais longo e propenso a erros. Você é livre para escolher o que achar mais legível.
Deseja testá-lo você mesmo? Aqui está o 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();
}
}
}
}
}
}
E os resultados:
--- for ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Os dados foram chamados 94 vezes e renderam 28153 itens.
--- para com lista ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Os dados foram chamados 1 vez (es) e renderam 575 itens.
--- --- enquanto
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Os dados foram chamados 1 vez (es) e renderam 47 item (s).
--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Os dados foram chamados 1 vez (es) e renderam 47 item (s).
LINQ vs. maneira tradicional
Quanto ao LINQ, você pode querer aprender programação funcional (FP) - não coisas de C # FP, mas linguagem FP real, como Haskell. Linguagens funcionais têm uma maneira específica de expressar e apresentar o código. Em algumas situações, é superior aos paradigmas não funcionais.
Sabe-se que o FP é muito superior quando se trata de manipular listas ( lista como um termo genérico, não relacionado a List<T>
). Dado esse fato, a capacidade de expressar o código C # de uma maneira mais funcional quando se trata de listas é uma coisa bastante boa.
Se você não está convencido, compare a legibilidade do código escrito de maneira funcional e não-funcional na minha resposta anterior sobre o assunto.