Filtrowanie pętli foreach z warunkiem gdzie vs klauzule ochronne

24

Widziałem, jak niektórzy programiści używają tego:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

zamiast tego, gdzie normalnie użyłbym:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Widziałem nawet kombinację obu. Bardzo podoba mi się czytelność z „kontynuuj”, szczególnie w bardziej złożonych warunkach. Czy jest jakaś różnica w wydajności? Zakładam, że z zapytaniem do bazy danych byłoby. Co ze zwykłymi listami?

Papryka
źródło
3
W przypadku zwykłych list brzmi to jak mikrooptymalizacja.
apokalipsa,
2
@zgnilec: ... ale tak naprawdę, który z dwóch wariantów jest wersją zoptymalizowaną? Oczywiście mam na ten temat opinię, ale samo spojrzenie na kod nie jest z natury jasne dla wszystkich.
Doc Brown,
2
Oczywiście kontynuacja będzie szybsza. Korzystanie z linq .Gdzie tworzysz dodatkowy iterator.
apokalipsa
1
@zgnilec - Dobra teoria. Chcesz opublikować odpowiedź wyjaśniającą, dlaczego tak myślisz? Obie istniejące odpowiedzi mówią wprost przeciwnie.
Bobson,
2
... więc sedno jest następujące: różnice wydajności między tymi dwoma konstruktami są zaniedbywalne, a czytelność i debugowanie można osiągnąć dla obu. To po prostu kwestia gustu, który wolisz.
Doc Brown,

Odpowiedzi:

64

Uznałbym to za odpowiednie miejsce do stosowania rozdzielania poleceń / zapytań . Na przykład:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

Pozwala to również na nadanie dobrej, samo dokumentującej nazwy wynikowi zapytania. Pomaga także dostrzec możliwości refaktoryzacji, ponieważ o wiele łatwiej jest refaktoryzować kod, który tylko wysyła zapytania do danych lub tylko mutuje dane, niż kod mieszany, który próbuje wykonać jedno i drugie.

Podczas debugowania możesz przerwać wcześniej, foreachaby szybko sprawdzić, czy zawartość jest validItemszgodna z oczekiwaniami. Nie musisz wchodzić w lambda, chyba że musisz. Jeśli musisz wejść w lambda, sugeruję, aby wydzielić ją na osobną funkcję, a następnie przejdź przez to.

Czy jest jakaś różnica w wydajności? Jeśli zapytanie jest wspierane przez bazę danych, wersja LINQ może potencjalnie działać szybciej, ponieważ zapytanie SQL może być bardziej wydajne. Jeśli jest to LINQ względem Objects, nie zobaczysz żadnej prawdziwej różnicy w wydajności. Jak zawsze, profiluj swój kod i napraw zgłaszane wąskie gardła, zamiast próbować z góry przewidywać optymalizacje.

Christian Hayter
źródło
1
Dlaczego tak duży zestaw danych miałby mieć takie znaczenie? Tylko dlatego, że minimalny koszt lambdas w końcu się sumuje?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft: Tak, masz rację, ten przykład nie wymaga dodatkowej złożoności algorytmicznej poza oryginalnym kodem. Usunąłem to wyrażenie.
Christian Hayter,
Czy nie powoduje to dwóch iteracji w kolekcji? Oczywiście drugi jest krótszy, biorąc pod uwagę, że znajdują się w nim tylko prawidłowe elementy, ale nadal musisz to zrobić dwa razy, raz, aby odfiltrować elementy, drugi raz, aby pracować z prawidłowymi elementami.
Andy,
1
@DavidPacker Nie. Prowadzony IEnumerablejest tylko przez foreachpętlę.
Benjamin Hodgson,
2
@DavidPacker: Właśnie to robi; większość metod LINQ to Objects jest implementowana przy użyciu bloków iteratora. Powyższy przykładowy kod będzie iterował kolekcję dokładnie raz, wykonując Wherelambda i ciało pętli (jeśli lambda zwróci wartość true) raz na element.
Christian Hayter,
7

Oczywiście istnieje różnica w wydajności, .Where()w wyniku czego dla każdego pojedynczego elementu wykonywane jest połączenie delegowane. Jednak nie martwiłbym się w ogóle wydajnością:

  • Cykle zegarowe używane podczas wywoływania delegata są pomijalne w porównaniu z cyklami zegarowymi używanymi przez resztę kodu, który iteruje kolekcję i sprawdza warunki.

  • Kara za wydajność wywołania delegata jest rzędu kilku cykli zegara i na szczęście już dawno minęły czasy, kiedy musieliśmy się martwić o poszczególne cykle zegara.

Jeśli z jakiegoś powodu wydajność jest dla Ciebie bardzo ważna na poziomie cyklu zegara, użyj List<Item>zamiast IList<Item>, aby kompilator mógł korzystać z bezpośrednich (i nieuchronnych) wywołań zamiast wywołań wirtualnych i aby iterator List<T>, który jest w rzeczywistości a struct, nie musi być zapakowane. Ale to naprawdę drobiazgi.

Zapytanie do bazy danych jest inną sytuacją, ponieważ istnieje (przynajmniej teoretycznie) możliwość wysłania filtra do RDBMS, co znacznie poprawia wydajność: tylko dopasowanie wierszy spowoduje przejście z RDBMS do twojego programu. Ale do tego myślę, że musiałbyś użyć linq, nie sądzę, aby to wyrażenie mogło zostać wysłane do RDBMS w obecnej formie.

Naprawdę zobaczysz korzyści z if(x) continue;momentu, w którym będziesz musiał debugować ten kod: Pojedyncze przeskakiwanie między if()s i continues ładnie działa; pojedynczy krok do filtrowania delegata jest uciążliwy.

Mike Nakis
źródło
Wtedy coś jest nie tak i chcesz spojrzeć na wszystkie elementy i sprawdzić w debuggerze, które mają Field! = Null, a które mają State! = Null; może to być trudne do niemożliwego z Foreach ... gdzie.
gnasher729,
Dobra uwaga przy debugowaniu. Wejście w miejsce nie jest takie złe w Visual Studio, ale nie można przepisać wyrażeń lambda podczas debugowania bez ponownej kompilacji, a tego unika się podczas używania if(x) continue;.
Paprik,
Ściśle mówiąc, .Wherewywoływana jest tylko raz. Co zostanie wywołana w każdej iteracji jest delegatem filtr (a MoveNexti Currentna wyliczający, gdy nie otrzymasz zoptymalizowaną out)
CodesInChaos
@CodesInChaos zajęło mi trochę czasu, aby zrozumieć, o czym mówisz, ale oczywiście, po co, masz rację, ściśle mówiąc, wywołuje się .Wheretylko raz. Naprawione.
Mike Nakis,