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
| Operator | Działanie | Zwraca |
|---|---|---|
Where(x => cond) | Filtrowanie | Sekwencję (odroczoną) |
Select(x => expr) | Przekształcenie każdego elementu | Sekwencję (odroczoną) |
SelectMany(x => seq) | Spłaszczenie zagnieżdżonych sekwencji | Sekwencję (odroczoną) |
OrderBy, ThenBy (+Descending) | Sortowanie stabilne | Sekwencję (odroczoną) |
GroupBy(x => key) | Podział na kubełki według klucza | Sekwencję grup (odroczoną) |
Distinct, Take, Skip | Usuwanie duplikatów, stronicowanie | Sekwencję (odroczoną) |
First, Single, Last | Jeden element, wyjątek, gdy brak | Element |
FirstOrDefault itd. | Jeden element albo wartość domyślna | Element |
Any, All, Contains | Test | bool |
Count, Sum, Min, Max, Average | Agregacja | Liczbę |
ToList, ToArray, ToDictionary | Wykonanie i zapisanie | Kolekcję |
Typowe błędy
First()na sekwencji, która może być pusta. Rzuca "Sequence contains no elements"; użyjFirstOrDefaulti sprawdź wynik.- Dwa wywołania
OrderBy. Drugi klucz staje się głównym; użyjThenBy. - 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,Maxna pustej sekwencji. Rzucają wyjątek; zabezpiecz się przezAny()alboDefaultIfEmpty.Count() > 0do 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.