for
vs. foreach
Istnieje powszechne zamieszanie, że te dwie konstrukcje są bardzo podobne i że oba są wymienne w następujący sposób:
foreach (var c in collection)
{
DoSomething(c);
}
i:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
Fakt, że oba słowa kluczowe zaczynają się od tych samych trzech liter, nie oznacza, że semantycznie są one podobne. To zamieszanie jest bardzo podatne na błędy, szczególnie dla początkujących. Iterowanie po kolekcji i robienie czegoś z elementami odbywa się foreach
; for
nie musi i nie powinien być wykorzystywany do tego celu , chyba że naprawdę wiesz, co robisz.
Zobaczmy, co jest nie tak z przykładem. Na końcu znajdziesz pełny kod aplikacji demonstracyjnej służącej do gromadzenia wyników.
W tym przykładzie ładujemy niektóre dane z bazy danych, a dokładniej miasta z Adventure Works, uporządkowane według nazwy, przed napotkaniem „Bostonu”. Używane jest następujące zapytanie SQL:
select distinct [City] from [Person].[Address] order by [City]
Dane są ładowane ListCities()
metodą, która zwraca an IEnumerable<string>
. Oto jak foreach
wygląda:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Przepiszmy to for
, zakładając, że oba są wymienne:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Oba zwracają te same miasta, ale jest ogromna różnica.
- Podczas używania
foreach
, ListCities()
nazywa się jeden raz i daje 47 pozycji.
- Podczas używania
for
, ListCities()
nazywa się 94 razy i daje 28153 pozycji w klasyfikacji generalnej.
Co się stało?
IEnumerable
jest leniwy . Oznacza to, że wykona pracę tylko w momencie, gdy wynik będzie potrzebny. Leniwa ocena jest bardzo przydatną koncepcją, ale ma pewne zastrzeżenia, w tym fakt, że łatwo przeoczyć moment (y), w których wynik będzie potrzebny, szczególnie w przypadkach, gdy wynik jest używany wielokrotnie.
W przypadku a foreach
żądanie jest wymagane tylko raz. W przypadku, for
jak zaimplementowano w niepoprawnie napisanym kodzie powyżej , żądanie jest wymagane 94 razy , tj. 47 × 2:
Za każdym razem cities.Count()
jest wywoływany (47 razy),
Za każdym razem cities.ElementAt(i)
jest wywoływany (47 razy).
Zapytanie do bazy danych 94 razy zamiast jednej jest okropne, ale nie najgorsze, co może się zdarzyć. Wyobraź sobie na przykład, co by się stało, gdyby select
zapytanie było poprzedzone zapytaniem, które również wstawia wiersz do tabeli. Tak, mielibyśmy for
wywołać bazę danych 2 147 483 647 razy, chyba że mam nadzieję, że wcześniej się zawiesi.
Oczywiście mój kod jest stronniczy. Celowo wykorzystałem lenistwo IEnumerable
i napisałem to w taki sposób, aby wielokrotnie dzwonić ListCities()
. Można zauważyć, że początkujący nigdy tego nie zrobi, ponieważ:
IEnumerable<T>
Nie posiada właściwości Count
, ale tylko metody Count()
. Wywołanie metody jest przerażające i można oczekiwać, że jej wynik nie będzie buforowany i nie będzie odpowiedni w for (; ...; )
bloku.
Indeksowanie jest niedostępne IEnumerable<T>
i nie jest oczywiste znalezienie ElementAt
metody rozszerzenia LINQ.
Prawdopodobnie większość początkujących przekonwertuje wynik ListCities()
na coś, co jest im znane, na przykład 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;
}
}
Mimo to ten kod bardzo różni się od foreach
alternatywy. Ponownie daje te same wyniki, a tym razem ListCities()
metoda jest wywoływana tylko raz, ale daje 575 pozycji, podczas gdy z foreach
daje tylko 47 pozycji.
Różnica wynika z faktu, ToList()
że wszystkie dane są ładowane z bazy danych. Mimo że foreach
prośba dotyczyła tylko miast sprzed „Bostonu”, nowa for
wymaga odzyskania wszystkich miast i zapisania ich w pamięci. Przy 575 krótkich ciągach, prawdopodobnie nie robi to dużej różnicy, ale co, jeśli pobieramy tylko kilka wierszy z tabeli zawierającej miliardy rekordów?
Więc co to jest foreach
naprawdę?
foreach
jest bliżej pętli while. Kod, którego wcześniej użyłem:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
można po prostu zastąpić:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Oba wytwarzają tę samą IL. Oba mają ten sam wynik. Oba mają takie same skutki uboczne. Oczywiście while
można to przepisać w podobnej nieskończoności for
, ale byłoby to jeszcze dłuższe i podatne na błędy. Możesz wybrać ten, który uważasz za bardziej czytelny.
Chcesz to przetestować sam? Oto pełny kod:
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();
}
}
}
}
}
}
A wyniki:
--- za ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dane nazwano 94 razy (s) i uzyskano 28153 pozycji.
--- dla z listą ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dane nazwano 1 raz (y) i uzyskano 575 pozycji.
--- podczas gdy ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dane nazwano 1 raz (y) i przyniosły 47 pozycji.
--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Dane nazwano 1 raz (y) i przyniosły 47 pozycji.
LINQ vs. tradycyjny sposób
Jeśli chodzi o LINQ, możesz nauczyć się programowania funkcjonalnego (FP) - nie rzeczy w języku C # FP, ale prawdziwy język FP, taki jak Haskell. Języki funkcjonalne mają określony sposób wyrażania i prezentacji kodu. W niektórych sytuacjach przewyższa paradygmaty niefunkcjonalne.
Wiadomo, że FP jest znacznie lepsza, jeśli chodzi o manipulowanie listami ( lista jako termin ogólny, niezwiązany z List<T>
). Biorąc pod uwagę ten fakt, możliwość wyrażania kodu C # w bardziej funkcjonalny sposób, jeśli chodzi o listy, jest raczej dobrą rzeczą.
Jeśli nie jesteś przekonany, porównaj czytelność kodu napisanego zarówno funkcjonalnie, jak i niefunkcjonalnie w mojej poprzedniej odpowiedzi na ten temat.