for
gegen foreach
Es besteht die allgemeine Verwirrung, dass diese beiden Konstrukte sehr ähnlich sind und dass beide wie folgt austauschbar sind:
foreach (var c in collection)
{
DoSomething(c);
}
und:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
Die Tatsache, dass beide Schlüsselwörter mit denselben drei Buchstaben beginnen, bedeutet nicht, dass sie semantisch ähnlich sind. Diese Verwirrung ist besonders für Anfänger äußerst fehleranfällig. Durch eine Sammlung zu iterieren und etwas mit den Elementen zu tun, ist erledigt mit foreach
; for
muss und sollte nicht für diesen Zweck verwendet werden , es sei denn, Sie wissen wirklich, was Sie tun.
Mal sehen, was daran falsch ist mit einem Beispiel. Am Ende finden Sie den vollständigen Code einer Demo-Anwendung, mit der die Ergebnisse gesammelt werden.
Im Beispiel laden wir einige Daten aus der Datenbank, genauer gesagt die Städte von Adventure Works, sortiert nach Namen, bevor wir auf "Boston" stoßen. Die folgende SQL-Abfrage wird verwendet:
select distinct [City] from [Person].[Address] order by [City]
Die Daten werden von der ListCities()
Methode geladen , die ein zurückgibt IEnumerable<string>
. So foreach
sieht es aus:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Lassen Sie es uns mit a umschreiben for
, vorausgesetzt, beide sind austauschbar:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Beide kehren in die gleichen Städte zurück, aber es gibt einen großen Unterschied.
- Bei der Verwendung
foreach
, ListCities()
wird einmal aufgerufen und liefert 47 Artikel.
- Bei der Verwendung
for
, ListCities()
94 mal aufgerufen und ergibt insgesamt 28.153 Artikel.
Was ist passiert?
IEnumerable
ist faul . Dies bedeutet, dass die Arbeit nur in dem Moment erledigt wird, in dem das Ergebnis benötigt wird. Lazy Evaluation ist ein sehr nützliches Konzept, hat jedoch einige Einschränkungen, einschließlich der Tatsache, dass es leicht ist, die Momente zu übersehen, in denen das Ergebnis benötigt wird, insbesondere in den Fällen, in denen das Ergebnis mehrmals verwendet wird.
In einem Fall von a foreach
wird das Ergebnis nur einmal angefordert. In einem Fall von a, for
wie in dem oben falsch geschriebenen Code implementiert , wird das Ergebnis 94-mal angefordert , dh 47 × 2:
Jedes Mal cities.Count()
wird aufgerufen (47 Mal),
Jedes Mal cities.ElementAt(i)
wird aufgerufen (47 Mal).
94-maliges Abfragen einer Datenbank ist schrecklich, aber nicht das Schlimmste, was passieren kann. Stellen Sie sich zum Beispiel vor, was passieren würde, wenn der select
Abfrage eine Abfrage vorangehen würde, die auch eine Zeile in die Tabelle einfügt. Richtig, wir hätten for
die Datenbank 2.147.483.647 Mal aufgerufen , es sei denn, sie stürzt hoffentlich vorher ab.
Natürlich ist mein Code voreingenommen. Ich habe absichtlich die Faulheit von genutzt IEnumerable
und es so geschrieben, dass es immer wieder anruft ListCities()
. Man kann feststellen, dass ein Anfänger dies niemals tun wird, weil:
Der IEnumerable<T>
hat nicht die Eigenschaft Count
, sondern nur die Methode Count()
. Das Aufrufen einer Methode ist beängstigend und es ist zu erwarten, dass das Ergebnis nicht zwischengespeichert und in einem for (; ...; )
Block nicht geeignet ist .
Die Indizierung ist für IEnumerable<T>
nicht verfügbar und es ist nicht offensichtlich, die ElementAt
LINQ-Erweiterungsmethode zu finden .
Wahrscheinlich würden die meisten Anfänger das Ergebnis einfach ListCities()
in etwas umwandeln, mit dem sie vertraut sind, wie z 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;
}
}
Dieser Code unterscheidet sich jedoch stark von der foreach
Alternative. Auch hier gibt es die gleichen Ergebnisse, und dieses Mal wird die ListCities()
Methode nur einmal aufgerufen, ergibt jedoch 575 Elemente, während sie mit foreach
nur 47 Elemente ergibt .
Der Unterschied besteht darin, ToList()
dass alle Daten aus der Datenbank geladen werden. Während foreach
nur die Städte vor "Boston" angefordert werden, müssen für die neue Stadt for
alle Städte abgerufen und gespeichert werden. Mit 575 kurzen Zeichenfolgen macht es wahrscheinlich keinen großen Unterschied, aber was wäre, wenn wir nur wenige Zeilen aus einer Tabelle mit Milliarden von Datensätzen abrufen würden?
Also, was ist das foreach
eigentlich?
foreach
ist näher an einer while-Schleife. Der Code, den ich zuvor verwendet habe:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
kann einfach ersetzt werden durch:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Beide produzieren die gleiche IL. Beide haben das gleiche Ergebnis. Beide haben die gleichen Nebenwirkungen. Natürlich while
kann dies in einer ähnlichen Unendlichkeit umgeschrieben werden for
, aber es wäre noch länger und fehleranfällig. Es steht Ihnen frei, diejenige zu wählen, die Sie besser lesen können.
Willst du es selbst testen? Hier ist der vollständige Code:
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();
}
}
}
}
}
}
Und die Ergebnisse:
--- für ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Die Daten wurden 94 Mal aufgerufen und ergaben 28153 Artikel.
--- für mit Liste ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Die Daten wurden 1 Mal aufgerufen und ergaben 575 Artikel.
--- während ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Die Daten wurden 1 Mal aufgerufen und ergaben 47 Artikel.
--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
Die Daten wurden 1 Mal aufgerufen und ergaben 47 Artikel.
LINQ vs. traditioneller Weg
Was LINQ betrifft, möchten Sie vielleicht Functional Programming (FP) lernen - nicht C # FP-Zeug, sondern echte FP-Sprache wie Haskell. In funktionalen Sprachen kann der Code auf bestimmte Weise ausgedrückt und dargestellt werden. In einigen Situationen ist es nicht-funktionalen Paradigmen überlegen.
FP ist dafür bekannt, dass es viel besser ist, Listen zu manipulieren ( Liste als Oberbegriff, unabhängig von List<T>
). Angesichts dieser Tatsache ist die Möglichkeit, C # -Code in Bezug auf Listen funktionaler auszudrücken, eher eine gute Sache.
Wenn Sie nicht überzeugt sind, vergleichen Sie die Lesbarkeit von Code, der sowohl funktional als auch nicht funktional in meiner vorherigen Antwort zu diesem Thema geschrieben wurde.