Menu

LINQ w C#: Where, Select, OrderBy, GroupBy i nie tylko

LINQ dodaje operatory zapytań do każdej kolekcji w C#. Poznaj składnię metod i składnię zapytań, filtrowanie przez Where, projekcję przez Select, sortowanie przez OrderBy i ThenBy, grupowanie przez GroupBy, pułapkę First a FirstOrDefault, agregacje i to, jak wykonanie odroczone zmienia moment uruchomienia zapytania.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

LINQ (Language Integrated Query) to biblioteka metod do filtrowania, przekształcania, sortowania, grupowania i podsumowywania sekwencji. Dodaj using System.Linq;, a każda tablica, List<T>, Dictionary i inny IEnumerable<T> zyskuje metody takie jak Where, Select i OrderBy, z których każda przyjmuje lambdę mówiącą, co zrobić z jednym elementem.

Pierwsze zapytanie: składnia metod i składnia zapytań

LINQ ma dwie notacje. Składnia metod łączy wywołania w łańcuch; składnia zapytań przypomina SQL. Kompilator zamienia składnię zapytań na te same wywołania metod, więc dają identyczne wyniki.

Wynik:

95, 91, 88
95, 91, 88

Składnia metod jest częstsza we współczesnym kodzie, a niektóre operatory (Take, Distinct, Count, First, ToList) istnieją tylko jako metody. Składnia zapytań błyszczy przy złączeniach i zapytaniach, które wprowadzają zmienne pośrednie przez let. Reszta tej strony używa składni metod, a składnię zapytań pokazuje tam, gdzie coś wnosi.

Where i Select: filtrowanie i projekcja

Where zachowuje elementy spełniające warunek. Select zamienia każdy element na coś innego. Razem pokrywają większość codziennych zapytań.

Wynik:

Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug

Where nigdy nie zmienia elementów, a Select nigdy nie zmienia ich liczby. new { p.Name, WithTax = ... } tworzy typ anonimowy, czyli klasę, którą kompilator pisze za ciebie, a jej właściwości odczytujesz zwykłą składnią z kropką. Nie ma nazwy, którą można zapisać, więc zmienną, która go przechowuje, trzeba zadeklarować przez var; pasuje do wyników, które nie wychodzą poza jedną metodę.

OrderBy, ThenBy i sortowanie według kilku kluczy

Wynik:

Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana

OrderBy jest stabilne: elementy z równymi kluczami zachowują pierwotną kolejność. Do drugiego klucza użyj ThenBy albo ThenByDescending. Drugie OrderBy posortowałoby wszystko od nowa według własnego klucza, a pierwszy klucz zostałby tylko rozstrzygaczem remisów: priorytet odwrotny do zamierzonego. W składni zapytań klucze oddziela się przecinkami: orderby o.Customer, o.Total descending.

W przeciwieństwie do List<T>.Sort, OrderBy nie rusza źródła i zwraca nową sekwencję.

First, Single, Last i ich wersje OrDefault

Zwracają jeden element zamiast sekwencji. Zwykłe wersje rzucają wyjątek, gdy nie ma czego zwrócić; wersje OrDefault zwracają wartość domyślną typu.

Wynik:

Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0

Wyjątek z First na pustej sekwencji to InvalidOperationException z komunikatem "Sequence contains no elements" (albo "Sequence contains no matching element", gdy predykat nic nie dopasował), jeden z najczęściej wyszukiwanych błędów C#. Używaj FirstOrDefault, gdy "brak" to normalny wynik, i sprawdzaj null.

Ostatnia linia pokazuje pułapkę typów wartościowych: dla sekwencji int metoda FirstOrDefault zwraca 0, gdy nic nie pasuje, a tego nie da się odróżnić od prawdziwego 0. Filtruj przez Where i najpierw sprawdź Any() albo, w .NET 6 i nowszych, przekaż własną wartość domyślną: scores.FirstOrDefault(s => s > 100, -1).

Single służy do przypadku "dokładnie jeden": rzuca wyjątek, gdy nie ma żadnego albo jest więcej niż jeden, co czyni go w równym stopniu asercją, co zapytaniem.

Any, All, Count i Contains

Wynik:

True
True
True
3
True
True

Any() zatrzymuje się na pierwszym dopasowaniu, więc items.Any() to właściwy sposób, by zapytać "czy coś tu jest?". items.Count() > 0 odczytuje właściwość Count, gdy źródło jest kolekcją, taką jak lista lub tablica, ale na zapytaniu albo innej leniwej sekwencji najpierw przechodzi przez każdy element. All na pustej sekwencji zwraca true, bo nie ma elementu, który nie spełnia warunku.

Sum, Average, Min i Max

Wynik:

Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException

Sum pustej sekwencji to 0, ale Average, Min i Max pustej sekwencji liczb rzucają InvalidOperationException: nie ma sensownej odpowiedzi. Zabezpiecz się przez Any() albo najpierw użyj DefaultIfEmpty(): empty.DefaultIfEmpty(0).Average().

Distinct, Take i Skip

Wynik:

home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15

Distinct zachowuje pierwsze wystąpienie każdej wartości w kolejności. Skip(n).Take(m) to standardowy wzorzec stronicowania, a TakeWhile/SkipWhile kończą lub zaczynają na pierwszym elemencie, który nie spełnia warunku. Enumerable.Range(start, count) generuje sekwencję liczb całkowitych bez tablicy.

GroupBy

GroupBy rozkłada elementy do kubełków według klucza. Każda grupa ma Key i sama jest sekwencją, więc możesz ją agregować.

Wynik:

North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev

Grupy pojawiają się w kolejności pierwszego wystąpienia ich kluczy w źródle (North, South, East), a nie posortowane; dodaj OrderBy(g => g.Key), jeśli tego potrzebujesz. W składni zapytań group X by K wybiera, co trafia do każdej grupy (tu tylko imię przedstawiciela), a into g nadaje grupie nazwę, żeby zapytanie mogło być kontynuowane.

ToList, ToArray i ToDictionary

Metody To... wykonują zapytanie i zapisują wyniki.

Wynik:

ben@x.com
2
ToDictionary: ArgumentException

ToDictionary rzuca ArgumentException, gdy dwa elementy dają ten sam klucz, tak jak Dictionary.Add. Jeśli spodziewasz się duplikatów, najpierw pogrupuj (jak robi to wordCounts) albo użyj ToLookup, które mapuje każdy klucz na sekwencję wartości.

Wykonanie odroczone

Where, Select, OrderBy, GroupBy i większość innych operatorów zwraca obiekt zapytania i nic nie robi, dopóki coś go nie wyliczy. Ma to dwie konsekwencje, które warto zobaczyć:

Wynik:

25, 40, 99
  converting 10
  converting 25
  converting 40
  converting 99
Count: 4
  converting 10
  converting 25
  converting 40
  converting 99
Max: 9900
  converting 10
  converting 25
  converting 40
  converting 99
Cached: 4, 9900

Pierwsze zapytanie zobaczyło 99, choć dodano je po napisaniu zapytania: filtr wykonał się, gdy string.Join je wyliczył. Drugie zapytanie wypisało swoje konwersje dwa razy, bo Count() i Max() wyliczyły je od początku, każde osobno. Przy taniej lambdzie to tylko zmarnowana praca; przy zapytaniu do bazy danych, odczycie pliku albo liczbie losowej w lambdzie oba przejścia mogą zwrócić różne dane. Gdy użyjesz wyniku więcej niż raz, zmaterializuj go przez ToList() albo ToArray().

Operatory zwracające pojedynczą wartość (Count, Sum, First, Any, Max) oraz metody To... wykonują się natychmiast. Jak zbudowana jest ta leniwość, pokazuje strona o IEnumerable i yield.

Ściągawka

OperatorDziałanieZwraca
Where(x => cond)FiltrowanieSekwencję (odroczoną)
Select(x => expr)Przekształcenie każdego elementuSekwencję (odroczoną)
SelectMany(x => seq)Spłaszczenie zagnieżdżonych sekwencjiSekwencję (odroczoną)
OrderBy, ThenBy (+Descending)Sortowanie stabilneSekwencję (odroczoną)
GroupBy(x => key)Podział na kubełki według kluczaSekwencję grup (odroczoną)
Distinct, Take, SkipUsuwanie duplikatów, stronicowanieSekwencję (odroczoną)
First, Single, LastJeden element, wyjątek, gdy brakElement
FirstOrDefault itd.Jeden element albo wartość domyślnaElement
Any, All, ContainsTestbool
Count, Sum, Min, Max, AverageAgregacjaLiczbę
ToList, ToArray, ToDictionaryWykonanie i zapisanieKolekcję

Typowe błędy

  • First() na sekwencji, która może być pusta. Rzuca "Sequence contains no elements"; użyj FirstOrDefault i sprawdź wynik.
  • Dwa wywołania OrderBy. Drugi klucz staje się głównym; użyj ThenBy.
  • Wielokrotne wyliczanie zapytania. Praca się powtarza; wywołaj raz ToList().
  • Oczekiwanie, że zapytanie zrobi migawkę źródła. Czyta źródło w chwili wyliczenia, a nie zapisania.
  • Average, Min, Max na pustej sekwencji. Rzucają wyjątek; zabezpiecz się przez Any() albo DefaultIfEmpty.
  • Count() > 0 do sprawdzania, czy są elementy. Any() zatrzymuje się na pierwszym.

Najczęściej zadawane pytania

Czym jest LINQ w C#?

LINQ (Language Integrated Query) to zestaw metod rozszerzających z System.Linq, takich jak Where, Select, OrderBy i GroupBy, które działają na każdym IEnumerable<T>: tablicach, listach, słownikach i wynikach innych zapytań. C# ma też składnię zapytań (from x in items where ... select ...), którą kompilator tłumaczy na te same wywołania metod.

Czym różni się Select od Where w LINQ?

Where filtruje: zachowuje elementy, dla których lambda zwraca true, i zostawia je bez zmian. Select robi projekcję: zamienia każdy element na coś innego, na przykład właściwość albo nowy obiekt, i zachowuje ich liczbę. Często łączy się je w łańcuch: people.Where(p => p.Age >= 18).Select(p => p.Name).

Czym różni się First od FirstOrDefault?

First() zwraca pierwszy element (albo pierwsze dopasowanie predykatu) i rzuca InvalidOperationException ("Sequence contains no elements" albo "no matching element"), gdy go nie ma. FirstOrDefault() zwraca wtedy wartość domyślną typu: null dla klas i 0 dla int, więc sprawdź wynik przed użyciem.

Jak w LINQ sortować według dwóch pól?

Po OrderBy dołącz ThenBy: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). Dwukrotne wywołanie OrderBy nie działa: drugie wywołanie sortuje całą sekwencję od nowa, więc jego klucz staje się główny, a pierwszy klucz rozstrzyga tylko remisy, czyli odwrotnie, niż zamierzano.

Jak działa GroupBy w LINQ?

GroupBy(x => key) zwraca jedną grupę na każdy unikalny klucz. Każda grupa to IGrouping<TKey, TElement>: ma właściwość Key i sama jest sekwencją elementów z tym kluczem, więc możesz na niej wywołać Count(), Sum() albo Select. Grupy pojawiają się w kolejności pierwszego wystąpienia ich kluczy.

Czym jest wykonanie odroczone w LINQ?

Większość operatorów LINQ nie wykonuje się w chwili wywołania; zwracają zapytanie, które wykonuje się przy każdym wyliczeniu (przez foreach, ToList(), Count() i tak dalej). Zapytanie widzi więc zmiany w źródle wprowadzone po jego zdefiniowaniu, a dwukrotne wyliczenie wykonuje pracę dwa razy. Wywołaj ToList() albo ToArray(), aby wykonać je raz i zachować wyniki.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ