it-swarm.com.de

für vs. foreach vs. LINQ

Wenn ich Code in Visual Studio schreibe, schlägt mir ReSharper (Gott segne es!) Oft vor, meine for-Schleife der alten Schule in der kompakteren foreach-Form zu ändern.

Und oft, wenn ich diese Änderung akzeptiere, geht ReSharper einen Schritt vorwärts und schlägt mir vor, sie erneut in einer glänzenden LINQ-Form zu ändern.

Ich frage mich also: Gibt es einige echte Vorteile bei diesen Verbesserungen? Bei einer ziemlich einfachen Codeausführung kann ich (offensichtlich) keinen Geschwindigkeitsschub sehen, aber ich kann sehen, dass der Code immer weniger lesbar wird ... Also frage ich mich: Lohnt es sich?

87
beccoblu

for vs. foreach

Es besteht die allgemeine Verwirrung, dass diese beiden Konstrukte sehr ähnlich sind und dass beide so 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. Das Durchlaufen einer Sammlung und das Ausführen von Elementen mit den Elementen erfolgt 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, anhand eines Beispiels. Am Ende finden Sie den vollständigen Code einer Demo-Anwendung, mit der die Ergebnisse erfasst werden.

In diesem Beispiel laden wir einige Daten aus der Datenbank, genauer gesagt die nach Namen geordneten Städte von Adventure Works, 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 Methode ListCities() geladen, die einen IEnumerable<string> Zurückgibt. So sieht foreach aus:

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

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

Schreiben wir es mit einem for um, 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 geben die gleichen Städte zurück, aber es gibt einen großen Unterschied.

  • Bei Verwendung von foreach wird ListCities() einmal aufgerufen und ergibt 47 Elemente.
  • Bei Verwendung von for wird ListCities() 94-mal aufgerufen und ergibt insgesamt 28153 Elemente.

Was ist passiert?

IEnumerable ist faul . Dies bedeutet, dass die Arbeit nur in dem Moment ausgeführt wird, in dem das Ergebnis benötigt wird. Lazy Evaluation ist ein sehr nützliches Konzept, weist jedoch einige Einschränkungen auf, einschließlich der Tatsache, dass es leicht ist, den Moment (die Momente) zu übersehen, in denen das Ergebnis benötigt wird, insbesondere in den Fällen, in denen das Ergebnis mehrmals verwendet wird.

Bei einem foreach wird das Ergebnis nur einmal angefordert. Bei einem for wie im oben falsch geschriebenen Code implementiert wird das Ergebnis 94 Mal angefordert , dh 47 × 2:

  • Jedes Mal, wenn cities.Count() aufgerufen wird (47 Mal),

  • Jedes Mal, wenn cities.ElementAt(i) aufgerufen wird (47 Mal).

Das 94-fache Abfragen einer Datenbank anstelle einer Datenbank ist schrecklich, aber nicht das Schlimmste, was passieren kann. Stellen Sie sich zum Beispiel vor, was passieren würde, wenn der Abfrage select eine Abfrage vorausgeht, die auch eine Zeile in die Tabelle einfügt. Richtig, wir hätten for, das die Datenbank 2.147.483.647 mal aufruft, es sei denn, sie stürzt hoffentlich vorher ab.

Natürlich ist mein Code voreingenommen. Ich habe absichtlich die Faulheit von IEnumerable verwendet und sie so geschrieben, dass ListCities() wiederholt aufgerufen wird. Man kann feststellen, dass ein Anfänger das niemals tun wird, weil:

  • Das IEnumerable<T> Hat nicht die Eigenschaft Count, sondern nur die Methode Count(). Das Aufrufen einer Methode ist beängstigend, und man kann erwarten, dass das Ergebnis nicht zwischengespeichert wird und nicht für einen for (; ...; ) - Block 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 von ListCities() einfach in etwas konvertieren, mit dem sie vertraut sind, wie z. B. einen 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 Alternative foreach. Wiederum gibt es die gleichen Ergebnisse, und dieses Mal wird die Methode ListCities() nur einmal aufgerufen , liefert jedoch 575 Elemente, während sie mit foreach ergibt nur 47 Artikel.

Der Unterschied ergibt sich aus der Tatsache, dass ToList() bewirkt, dass alle Daten aus der Datenbank geladen werden. Während foreach nur die Städte vor "Boston" angefordert hat, müssen für das neue 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?

Was ist eigentlich foreach?

foreach liegt 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 kann dieses while in einem ähnlichen unendlichen for umgeschrieben werden, aber es wäre noch länger und fehleranfälliger. Sie können diejenige auswählen, die Sie besser lesbar finden.

Möchten Sie 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:

--- zum ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Die Daten wurden als 94 Zeit (en) bezeichnet und ergaben 28153 Elemente.

--- für mit Liste ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Die Daten wurden 1 Mal aufgerufen und ergaben 575 Elemente.

--- während ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Die Daten wurden 1 Mal aufgerufen und ergaben 47 Artikel.

--- für jedes ---
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 die funktionale Programmierung (FP) lernen - nicht C # FP Zeug, sondern real = FP Sprache wie Haskell. Funktionale Sprachen haben eine bestimmte Art, den Code auszudrücken und darzustellen. In einigen Situationen ist er nicht funktionalen Paradigmen überlegen.

FP ist bekanntermaßen viel überlegen, wenn es darum geht, Listen zu manipulieren ( Liste als Oberbegriff, unabhängig von List<T>). Angesichts dieser Tatsache ist die Fähigkeit, C # -Code in Bezug auf Listen funktionaler auszudrücken, eine gute Sache.

Wenn Sie nicht überzeugt sind, vergleichen Sie die Lesbarkeit von Code, der sowohl funktional als auch nicht funktional geschrieben wurde, in meinem vorherige Antwort zu diesem Thema.

140

Zwar gibt es bereits einige großartige Darstellungen zu den Unterschieden zwischen for und foreach. Es gibt einige grobe Falschdarstellungen der Rolle von LINQ.

Die LINQ-Syntax ist nicht nur syntaktischer Zucker, der eine funktionale Programmiernäherung an C # liefert. LINQ bietet funktionale Konstrukte einschließlich aller Vorteile für C #. In Kombination mit der Rückgabe von IEnumerable anstelle von IList bietet LINQ eine verzögerte Ausführung der Iteration. Was die Leute jetzt normalerweise tun, ist, eine IList so zu konstruieren und von ihren Funktionen zurückzugeben

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

Verwenden Sie stattdessen Yield Return Syntax , um eine verzögerte Aufzählung zu erstellen.

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;
}

Jetzt wird die Aufzählung erst durchgeführt, wenn Sie eine Liste erstellen oder darüber iterieren. Und es tritt nur bei Bedarf auf (hier ist eine Aufzählung von Fibbonaci, die kein Stapelüberlaufproblem hat)

/**
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;
  }
}

Wenn Sie einen foreach über die Fibonacci-Funktion ausführen, wird die Folge 46 zurückgegeben. Wenn Sie den 30. möchten, wird nur dieser berechnet

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

Wo wir viel Spaß haben, ist die Unterstützung in der Sprache für Lambda-Ausdrücke (in Kombination mit den Konstrukten IQueryable und IQueryProvider ermöglicht dies die funktionale Komposition von Abfragen anhand einer Vielzahl von Datensätzen. Der IQueryProvider ist für die Interpretation der übergebenen Daten verantwortlich Ausdrücke und Erstellen und Ausführen einer Abfrage unter Verwendung der nativen Konstrukte der Quelle). Ich werde hier nicht auf die Details eingehen, aber es gibt eine Reihe von Blog-Posts, die zeigen, wie ein SQL Query Provider hier erstellt wird

Zusammenfassend sollten Sie IEnumerable gegenüber IList zurückgeben, wenn die Konsumenten Ihrer Funktion eine einfache Iteration durchführen. Verwenden Sie die Funktionen von LINQ, um die Ausführung komplexer Abfragen zu verschieben, bis sie benötigt werden.

19
Michael Brown

aber ich kann sehen, dass der Code immer weniger lesbar wird

Die Lesbarkeit liegt im Auge des Betrachters. Einige Leute könnten sagen

var common = list1.Intersect(list2);

ist perfekt lesbar; andere könnten sagen, dass dies undurchsichtig ist und es vorziehen würde

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;
        }
    }
}

um klarer zu machen, was getan wird. Wir können Ihnen nicht sagen, was Sie besser lesbar finden. Aber vielleicht können Sie in dem Beispiel, das ich hier konstruiert habe, einige meiner eigenen Vorurteile erkennen ...

13
AakashM

Der Unterschied zwischen LINQ und foreach beruht auf zwei verschiedenen Programmierstilen: imperativ und deklarativ.

  • Imperativ: In diesem Stil sagen Sie dem Computer "mach das ... jetzt mach das ... jetzt mach das jetzt mach das". Sie geben Schritt für Schritt ein Programm ein.

  • Deklarativ: In diesem Stil teilen Sie dem Computer mit, wie das Ergebnis aussehen soll, und lassen ihn herausfinden, wie er dorthin gelangt.

Ein klassisches Beispiel für diese beiden Stile ist der Vergleich von Assembly-Code (oder C) mit SQL. In Assembly geben Sie (buchstäblich) Anweisungen nacheinander. In SQL drücken Sie aus, wie Daten zusammengefügt werden sollen und welches Ergebnis Sie aus diesen Daten erzielen möchten.

Ein netter Nebeneffekt der deklarativen Programmierung ist, dass sie tendenziell etwas höher ist. Auf diese Weise kann sich die Plattform unter Ihnen weiterentwickeln, ohne dass Sie Ihren Code ändern müssen. Zum Beispiel:

var foo = bar.Distinct();

Was passiert hier? Verwendet Distinct einen Kern? Zwei? Fünfzig? Wir wissen es nicht und es ist uns egal. Die .NET-Entwickler können es jederzeit neu schreiben, solange es denselben Zweck erfüllt. Unser Code kann nach einer Code-Aktualisierung auf magische Weise schneller werden.

Dies ist die Kraft der funktionalen Programmierung. Und der Grund, warum Sie diesen Code in Sprachen wie Clojure, F # und C # (geschrieben mit einer funktionalen Programmier-Denkweise) finden, ist oft 3x-10x kleiner als seine zwingenden Gegenstücke.

Schließlich gefällt mir der deklarative Stil, da ich in C # meistens Code schreiben kann, der die Daten nicht mutiert. Im obigen Beispiel ändert Distinct() den Balken nicht, sondern gibt eine neue Kopie der Daten zurück. Dies bedeutet, dass sich jeder Balken, und wo immer er herkam, nicht plötzlich geändert hat.

Lernen Sie also, wie die anderen Poster sagen, funktionale Programmierung. Es wird dein Leben verändern. Und wenn Sie können, tun Sie dies in einer echten funktionalen Programmiersprache. Ich bevorzuge Clojure, aber F # und Haskell sind auch eine ausgezeichnete Wahl.

7