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?
foreach
jest usuwanie elementów z kolekcji podczas ich wyliczania, gdzie zwyklefor
potrzebna jest pętla, aby rozpocząć od ostatniego elementu.Odpowiedzi:
for
vs.foreach
Istnieje powszechne zamieszanie, że te dwie konstrukcje są bardzo podobne i że oba są wymienne w następujący sposób:
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:
Dane są ładowane
ListCities()
metodą, która zwraca anIEnumerable<string>
. Oto jakforeach
wygląda:Przepiszmy to
for
, zakładając, że oba są wymienne:Oba zwracają te same miasta, ale jest ogromna różnica.
foreach
,ListCities()
nazywa się jeden raz i daje 47 pozycji.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śmyfor
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ściCount
, ale tylko metodyCount()
. Wywołanie metody jest przerażające i można oczekiwać, że jej wynik nie będzie buforowany i nie będzie odpowiedni wfor (; ...; )
bloku.Indeksowanie jest niedostępne
IEnumerable<T>
i nie jest oczywiste znalezienieElementAt
metody rozszerzenia LINQ.Prawdopodobnie większość początkujących przekonwertuje wynik
ListCities()
na coś, co jest im znane, na przykładList<T>
.Mimo to ten kod bardzo różni się od
foreach
alternatywy. Ponownie daje te same wyniki, a tym razemListCities()
metoda jest wywoływana tylko raz, ale daje 575 pozycji, podczas gdy zforeach
daje tylko 47 pozycji.Różnica wynika z faktu,
ToList()
że wszystkie dane są ładowane z bazy danych. Mimo żeforeach
prośba dotyczyła tylko miast sprzed „Bostonu”, nowafor
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:można po prostu zastąpić:
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ścifor
, 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:
A wyniki:
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.
źródło
foreach
jest 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.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
Zamiast tego należy użyć składni zwrotu zysku, aby utworzyć odroczone wyliczenie.
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)
Wykonanie foreach nad funkcją Fibonacciego zwróci sekwencję 46. Jeśli chcesz 30. to wszystko, co zostanie obliczone
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.
źródło
Czytelność zależy od obserwatora. Niektórzy mogą powiedzieć
jest doskonale czytelny; inni mogą powiedzieć, że jest nieprzejrzysty i wolałby
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 ...
źródło
Różnica między LINQ a
foreach
naprawdę 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:
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.
źródło
var foo = bar.Distinct()
jest zasadniczo doIEnumerator<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.Czy inni programiści w zespole mogą czytać LINQ?
Jeśli nie, nie używaj go, bo wydarzy się jedna z dwóch rzeczy:
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.
źródło