dla vs foreach vs. LINQ

86

Kiedy piszę kod w Visual Studio, ReSharper (niech Bóg błogosławi!) Często sugeruje mi zmianę starej pętli for w bardziej zwartej formie foreach.

I często, kiedy akceptuję tę zmianę, ReSharper robi krok do przodu i sugeruje, żebym ją ponownie zmienił, w błyszczącej formie LINQ.

Więc zastanawiam się: czy istnieją jakieś realne korzyści, w tych ulepszeń? W dość prostym wykonaniu kodu nie widzę żadnego przyspieszenia (oczywiście), ale widzę, że kod staje się coraz mniej czytelny ... Zastanawiam się: czy warto?

beccoblu
źródło
2
Tylko uwaga - składnia LINQ jest właściwie dość czytelna, jeśli znasz składnię SQL. Istnieją również dwa formaty LINQ (wyrażenia lambda podobne do SQL i metody łańcuchowe), które mogą ułatwić naukę. Mogą to być po prostu sugestie ReSharpera, które sprawiają, że wydaje się to nieczytelne.
Shauna,
3
Zasadniczo zwykle używam foreach, chyba że pracuję ze znaną tablicą długości lub podobnymi przypadkami, w których liczba iteracji jest istotna. Co do LINQ-ifying, zwykle widzę, co ReSharper robi z foreach, a jeśli wynikowa instrukcja LINQ jest uporządkowana / trywialna / czytelna, używam jej, a w przeciwnym razie cofam ją. Jeśli byłoby to obowiązkowe ponowne zapisanie oryginalnej logiki innej niż LINQ, jeśli wymagania ulegną zmianie lub może być konieczne szczegółowe debugowanie logiki, z której wyciąga się instrukcja LINQ, nie LINQ jej i nie pozostawiam jej na długo Formularz.
Ed Hastings,
częstym błędem foreachjest usuwanie elementów z kolekcji podczas ich wyliczania, gdzie zwykle forpotrzebna jest pętla, aby rozpocząć od ostatniego elementu.
Slai,
Możesz wziąć wartość z Øredev 2013 - Jessica Kerr - Zasady funkcjonalne dla programistów zorientowanych obiektowo . Linq wchodzi do prezentacji wkrótce po 33 minutowym znaku, pod tytułem „Styl deklaratywny”.
Theraot

Odpowiedzi:

139

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; fornie 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 foreachwyglą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?

IEnumerablejest 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 selectzapytanie było poprzedzone zapytaniem, które również wstawia wiersz do tabeli. Tak, mielibyśmy forwywoł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 IEnumerablei 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 ElementAtmetody 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 foreachalternatywy. Ponownie daje te same wyniki, a tym razem ListCities()metoda jest wywoływana tylko raz, ale daje 575 pozycji, podczas gdy z foreachdaje tylko 47 pozycji.

Różnica wynika z faktu, ToList()że wszystkie dane są ładowane z bazy danych. Mimo że foreachprośba dotyczyła tylko miast sprzed „Bostonu”, nowa forwymaga 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 foreachnaprawdę?

foreachjest 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 whilemoż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.

Arseni Mourzenko
źródło
1
Pytanie o przykład ListCities (). Dlaczego miałby działać tylko raz? W przeszłości nie miałem problemów z przepowiadaniem o zyskach.
Dante,
1
Nie mówi, że uzyskasz tylko jeden wynik z IEnumerable - twierdzi, że zapytanie SQL (które jest kosztowną częścią metody) wykona się tylko raz - to dobra rzecz. Następnie odczyta i zwróci wszystkie wyniki zapytania.
HappyCat,
9
@Giorgio: Chociaż to pytanie jest zrozumiałe, semantyka języka jest pandemiczna dla tego, co początkujący może wprawić w zakłopotanie, ale nie pozostawi nam bardzo skutecznego języka.
Steven Evers,
4
LINQ to nie tylko cukier semantyczny. Zapewnia opóźnione wykonanie. A w przypadku IQueryables (np. Entity Framework) pozwala na przesłanie zapytania i skomponowanie go do iteracji (co oznacza, że ​​dodanie klauzuli where do zwróconego IQueryable spowoduje, że SQL zostanie przekazany do serwera po iteracji, aby uwzględnić klauzulę where odciążenie filtrowania na serwerze).
Michael Brown,
8
Chociaż podoba mi się ta odpowiedź, wydaje mi się, że przykłady są wymyślone. Podsumowanie na końcu sugeruje, że foreachjest to bardziej wydajne niż for, gdy w rzeczywistości rozbieżność wynika z celowo zepsutego kodu. Dokładność odpowiedzi sama się odkupia, ale łatwo zauważyć, jak przypadkowy obserwator może dojść do błędnych wniosków.
Robert Harvey,
19

Chociaż istnieją już świetne ekspozycje na temat różnic między forami i foreach. Istnieją poważne rażące informacje na temat roli LINQ.

Składnia LINQ to nie tylko cukier składniowy, który daje funkcjonalne przybliżenie programowania do C #. LINQ zapewnia konstrukty funkcjonalne, w tym wszystkie zalety C #. W połączeniu ze zwracaniem IEnumerable zamiast IList, LINQ zapewnia odroczone wykonanie iteracji. To, co ludzie zwykle robią teraz, to konstruowanie i zwracanie IList z ich funkcji w ten sposób

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

Zamiast tego należy użyć składni zwrotu zysku, aby utworzyć odroczone wyliczenie.

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

Teraz wyliczenie nie wystąpi, dopóki nie wykonasz ToList lub nie wykonasz iteracji. I to występuje tylko w razie potrzeby (oto wyliczenie Fibbonaci, które nie ma problemu z przepełnieniem stosu)

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

Wykonanie foreach nad funkcją Fibonacciego zwróci sekwencję 46. Jeśli chcesz 30. to wszystko, co zostanie obliczone

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

Tam, gdzie możemy się dobrze bawić, wsparcie w języku dla wyrażeń lambda (w połączeniu z konstrukcjami IQueryable i IQueryProvider, pozwala to na funkcjonalną kompozycję zapytań względem różnych zestawów danych, IQueryProvider jest odpowiedzialny za interpretację przekazywanych w wyrażenia oraz tworzenie i wykonywanie zapytania przy użyciu rodzimych konstrukcji źródła). Nie będę tutaj wchodził w szczegóły drobiazgów, ale istnieje szereg postów na blogu pokazujących, jak utworzyć dostawcę zapytań SQL

Podsumowując, powinieneś preferować zwracanie IEnumerable zamiast IList, gdy odbiorcy twojej funkcji wykonają prostą iterację. I wykorzystaj możliwości LINQ, aby odroczyć wykonywanie złożonych zapytań, dopóki nie będą potrzebne.

Michael Brown
źródło
13

ale widzę, że kod staje się coraz mniej czytelny

Czytelność zależy od obserwatora. Niektórzy mogą powiedzieć

var common = list1.Intersect(list2);

jest doskonale czytelny; inni mogą powiedzieć, że jest nieprzejrzysty i wolałby

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

jako wyjaśnienie, co się dzieje. Nie możemy ci powiedzieć, co uważasz za bardziej czytelne. Ale możesz wykryć niektóre z moich uprzedzeń w przykładzie, który tutaj skonstruowałem ...

AakashM
źródło
28
Szczerze mówiąc, powiedziałbym, że Linq sprawia, że ​​intencja jest bardziej czytelna, podczas gdy w przypadku pętli mechanizm ten jest bardziej czytelny.
jk.
16
Uciekłbym tak szybko, jak to tylko możliwe, od kogoś, kto powiedziałby mi, że wersja for-for-if jest bardziej czytelna niż wersja przecinająca się.
Konamiman
3
@Konamiman - To zależy od tego, czego szuka osoba, gdy myśli o „czytelności”. Komentarz jk. doskonale to ilustruje. Pętla jest bardziej czytelna w tym sensie, że można łatwo zobaczyć, jak osiąga swój wynik końcowy, podczas gdy LINQ jest bardziej czytelny pod względem tego, jaki powinien być wynik końcowy.
Shauna,
2
Właśnie dlatego pętla przechodzi do implementacji, a następnie wszędzie korzystasz z Intersect.
R. Martinho Fernandes,
8
@Shauna: Wyobraź sobie wersję for-loop wewnątrz metody wykonującej kilka innych rzeczy; to bałagan. Więc oczywiście podzieliłeś go na własną metodę. Pod względem czytelności jest to to samo, co IEnumerable <T> .Intersect, ale teraz zduplikowano funkcjonalność frameworka i wprowadzono więcej kodu do utrzymania. Jedynym usprawiedliwieniem jest to, że potrzebujesz niestandardowej implementacji ze względów behawioralnych, ale mówimy tu tylko o czytelności.
Misko
7

Różnica między LINQ a foreachnaprawdę sprowadza się do dwóch różnych stylów programowania: imperatywnego i deklaratywnego.

  • Konieczne: w tym stylu mówisz komputerowi „zrób to ... teraz zrób to ... teraz zrób to teraz zrób to”. Podajesz mu program krok po kroku.

  • Deklaratywny: w tym stylu informujesz komputer, jaki ma być wynik, i pozwalasz mu dowiedzieć się, jak się tam dostać.

Klasycznym przykładem tych dwóch stylów jest porównanie kodu asemblera (lub C) z SQL. Podczas montażu podajesz instrukcje (dosłownie) pojedynczo. W SQL wyrażasz, jak łączyć dane razem i jaki wynik chcesz z tych danych.

Przyjemnym efektem ubocznym programowania deklaratywnego jest to, że jest on nieco wyższy. Pozwala to platformie ewoluować pod tobą bez konieczności zmiany kodu. Na przykład:

var foo = bar.Distinct();

Co tu się dzieje? Czy Distinct używa jednego rdzenia? Dwa? Pięćdziesiąt? Nie wiemy i nie obchodzi nas to. Deweloperzy .NET mogliby go przepisać w dowolnym momencie, o ile nadal będą wykonywać te same cele, które nasz kod może po prostu magicznie przyspieszyć po aktualizacji kodu.

To jest siła programowania funkcjonalnego. A powód, dla którego znajdziesz ten kod w językach takich jak Clojure, F # i C # (napisany z funkcjonalnym nastawieniem programistycznym) jest często 3 x 10 razy mniejszy niż jego bezwzględny odpowiednik.

Wreszcie podoba mi się styl deklaratywny, ponieważ w C # przez większość czasu pozwala mi to pisać kod, który nie powoduje mutacji danych. W powyższym przykładzie Distinct()nie zmienia paska, zwraca nową kopię danych. Oznacza to, że niezależnie od tego, jaki jest pasek i skądkolwiek pochodził, nie zmienił się nagle.

Tak jak mówią inne plakaty, naucz się programowania funkcjonalnego. Zmieni twoje życie. A jeśli możesz, zrób to w prawdziwym funkcjonalnym języku programowania. Wolę Clojure, ale F # i Haskell to również doskonały wybór.

Timothy Baldridge
źródło
2
Przetwarzanie LINQ jest odraczane do momentu, aż faktycznie się nad nim powtórzy. var foo = bar.Distinct()jest zasadniczo do IEnumerator<T>czasu, aż zadzwonisz .ToList()lub .ToArray(). To ważne rozróżnienie, ponieważ jeśli nie jesteś tego świadomy, może to prowadzić do trudnych do zrozumienia błędów.
Berin Loritsch
-5

Czy inni programiści w zespole mogą czytać LINQ?

Jeśli nie, nie używaj go, bo wydarzy się jedna z dwóch rzeczy:

  1. Twój kod będzie niemożliwy do utrzymania
  2. Utkniesz z utrzymywaniem całego kodu i wszystkiego, co na nim polega

A dla każdej pętli jest idealny do iteracji po liście, ale jeśli to nie to, co musisz zrobić, nie używaj jej.

Odwrócona lama
źródło
11
hmm, doceniam, że dla jednego projektu może to być odpowiedź, ale w perspektywie średnio- i długoterminowej powinieneś szkolić swoich pracowników, w przeciwnym razie masz wyścig do samego końca rozumienia kodu, co nie brzmi jak dobry pomysł.
jk.
21
W rzeczywistości może się zdarzyć trzecia rzecz: inni programiści mogą włożyć niewielki wysiłek i nauczyć się czegoś nowego i pożytecznego. Nie jest to niespotykane.
Eric King,
6
@InvertedLlama, gdybym był w firmie, w której programiści potrzebują formalnego szkolenia, aby zrozumieć nowe koncepcje językowe, pomyślałbym o znalezieniu nowej firmy.
Wyatt Barnett,
13
Być może możesz uciec od takiego nastawienia w bibliotekach, ale jeśli chodzi o podstawowe funkcje językowe, to nie oznacza, że ​​nie jest to konieczne. Możesz wybierać frameworki. Ale dobry programista .NET musi zrozumieć każdą funkcję języka i platformy podstawowej (System. *). Biorąc pod uwagę, że nie możesz nawet poprawnie używać EF bez Linq, muszę powiedzieć ... w dzisiejszych czasach, jeśli jesteś programistą .NET i nie znasz Linq, jesteś niekompetentny.
Timothy Baldridge,
7
Ma to już dość głosów negatywnych, więc nie dodam do tego, ale argument wspierający ignorantów / niekompetentnych współpracowników nigdy nie jest ważny.
Steven Evers,