LINQ (Language Integrated Query) это библиотека методов для фильтрации, преобразования, сортировки, группировки и подведения итогов по последовательностям. Добавьте using System.Linq;, и каждый массив, List<T>, Dictionary и другой IEnumerable<T> получит методы вроде Where, Select и OrderBy, каждый из которых принимает лямбду, говорящую, что делать с одним элементом.
Первый запрос: синтаксис методов и синтаксис запросов
У LINQ две формы записи. Синтаксис методов объединяет вызовы в цепочку; синтаксис запросов читается как SQL. Компилятор превращает синтаксис запросов в те же вызовы методов, поэтому результаты одинаковы.
Вывод:
95, 91, 88
95, 91, 88
В современном коде чаще встречается синтаксис методов, а некоторые операторы (Take, Distinct, Count, First, ToList) существуют только как методы. Синтаксис запросов хорош для соединений и для запросов, которые вводят промежуточные переменные через let. Дальше на этой странице используется синтаксис методов, а синтаксис запросов показан там, где он что-то добавляет.
Where и Select: фильтрация и проекция
Where оставляет элементы, удовлетворяющие условию. Select превращает каждый элемент во что-то другое. Вместе они покрывают большинство повседневных запросов.
Вывод:
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 никогда не меняет элементы, а Select никогда не меняет их количество. new { p.Name, WithTax = ... } создаёт анонимный тип, класс, который компилятор пишет за вас, и его свойства читаются через обычную точку. У него нет имени, которое можно написать, поэтому переменную с таким значением нужно объявлять через var, и он подходит для результатов, которые не выходят за пределы одного метода.
OrderBy, ThenBy и сортировка по нескольким ключам
Вывод:
Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana
OrderBy стабилен: элементы с равными ключами остаются в исходном порядке. Для второго ключа используйте ThenBy или ThenByDescending. Второй OrderBy пересортировал бы всё по своему ключу, оставив первому ключу лишь роль разрешения равенства: приоритет наоборот. В синтаксисе запросов ключи перечисляются через запятую: orderby o.Customer, o.Total descending.
В отличие от List<T>.Sort, OrderBy не трогает источник и возвращает новую последовательность.
First, Single, Last и их версии OrDefault
Они возвращают один элемент, а не последовательность. Обычные версии выбрасывают исключение, когда возвращать нечего; версии OrDefault возвращают значение типа по умолчанию.
Вывод:
Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0
Исключение от First на пустой последовательности это InvalidOperationException с сообщением «Sequence contains no elements» (или «Sequence contains no matching element», когда ничего не совпало с предикатом), одна из самых искомых ошибок C#. Используйте FirstOrDefault, когда «ничего» это нормальный исход, и проверяйте на null.
Последняя строка показывает ловушку с типами значений: для последовательности int метод FirstOrDefault возвращает 0, когда ничего не совпало, и это нельзя отличить от настоящего 0. Фильтруйте через Where и сначала проверяйте Any() или в .NET 6 и новее передайте собственное значение по умолчанию: scores.FirstOrDefault(s => s > 100, -1).
Single предназначен для случая «ровно один»: он выбрасывает исключение, если элементов нет или их больше одного, поэтому он столько же проверка, сколько запрос.
Any, All, Count и Contains
Вывод:
True
True
True
3
True
True
Any() останавливается на первом совпадении, поэтому items.Any() это правильный способ спросить «есть ли что-нибудь?». items.Count() > 0 читает свойство Count, когда источник это коллекция вроде списка или массива, но для запроса или другой ленивой последовательности сначала проходит все элементы. All для пустой последовательности возвращает true, потому что нет элемента, не выполняющего условие.
Sum, Average, Min и Max
Вывод:
Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException
Sum пустой последовательности равен 0, а Average, Min и Max для пустой последовательности чисел выбрасывают InvalidOperationException: разумного ответа нет. Защищайтесь через Any() или сначала используйте DefaultIfEmpty(): empty.DefaultIfEmpty(0).Average().
Distinct, Take и Skip
Вывод:
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 сохраняет первое вхождение каждого значения по порядку. Skip(n).Take(m) это стандартный шаблон постраничного вывода, а TakeWhile/SkipWhile останавливаются или начинают на первом элементе, не выполняющем условие. Enumerable.Range(start, count) генерирует последовательность целых чисел без массива.
GroupBy
GroupBy раскладывает элементы по корзинам по ключу. У каждой группы есть Key, и она сама является последовательностью, поэтому по ней можно считать агрегаты.
Вывод:
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
Группы выходят в том порядке, в каком их ключи впервые встречаются в источнике (North, South, East), а не отсортированными; если нужна сортировка, добавьте OrderBy(g => g.Key). В синтаксисе запросов group X by K выбирает, что попадает в каждую группу (здесь только имя продавца), а into g даёт группе имя, чтобы запрос мог продолжиться.
ToList, ToArray и ToDictionary
Методы To... выполняют запрос и сохраняют результаты.
Вывод:
ben@x.com
2
ToDictionary: ArgumentException
ToDictionary выбрасывает ArgumentException, когда два элемента дают одинаковый ключ, так же как Dictionary.Add. Если дубликаты ожидаемы, сначала сгруппируйте (как это делает wordCounts) или используйте ToLookup, который сопоставляет каждому ключу последовательность значений.
Отложенное выполнение
Where, Select, OrderBy, GroupBy и большинство других операторов возвращают объект запроса и не выполняют никакой работы, пока что-то его не перечислит. У этого два последствия, которые стоит увидеть:
Вывод:
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
Первый запрос увидел 99, хотя это значение добавили после написания запроса: фильтр выполнился, когда его перечислил string.Join. Второй запрос напечатал свои преобразования дважды, потому что Count() и Max() каждый перечисляли его с нуля. С дешёвой лямбдой это просто лишняя работа; с запросом к базе данных, чтением файла или случайным числом внутри лямбды два прохода могут вернуть разные данные. Когда результат нужен больше одного раза, материализуйте его через ToList() или ToArray().
Операторы, которые возвращают одно значение (Count, Sum, First, Any, Max), и методы To... выполняются сразу. Как устроена эта ленивость, описано на странице IEnumerable и yield.
Краткая справка
| Оператор | Что делает | Возвращает |
|---|---|---|
Where(x => cond) | Фильтрует | Последовательность (отложенно) |
Select(x => expr) | Преобразует каждый элемент | Последовательность (отложенно) |
SelectMany(x => seq) | Разворачивает вложенные последовательности | Последовательность (отложенно) |
OrderBy, ThenBy (+Descending) | Сортирует, стабильно | Последовательность (отложенно) |
GroupBy(x => key) | Раскладывает по ключу | Последовательность групп (отложенно) |
Distinct, Take, Skip | Убирает дубликаты, делит на страницы | Последовательность (отложенно) |
First, Single, Last | Один элемент, исключение, если нет | Элемент |
FirstOrDefault и др. | Один элемент или значение по умолчанию | Элемент |
Any, All, Contains | Проверка | bool |
Count, Sum, Min, Max, Average | Агрегат | Число |
ToList, ToArray, ToDictionary | Выполняет и сохраняет | Коллекция |
Частые ошибки
First()на последовательности, которая может быть пустой. Выбрасывает «Sequence contains no elements»; используйтеFirstOrDefaultи проверяйте.- Два вызова
OrderBy. Второй ключ становится главным; используйтеThenBy. - Многократное перечисление запроса. Работа повторяется; вызовите
ToList()один раз. - Ожидание, что запрос делает снимок источника. Он читает источник при перечислении, а не при написании.
Average,Min,Maxна пустой последовательности. Они выбрасывают исключение; защищайтесь черезAny()илиDefaultIfEmpty.Count() > 0для проверки наличия элементов.Any()останавливается на первом.
Часто задаваемые вопросы
Что такое LINQ в C#?
LINQ (Language Integrated Query) это набор методов расширения из System.Linq, таких как Where, Select, OrderBy и GroupBy, которые работают с любым IEnumerable<T>: массивами, списками, словарями и результатами других запросов. В C# есть также синтаксис запросов (from x in items where ... select ...), который компилятор переводит в те же вызовы методов.
Чем Select отличается от Where в LINQ?
Where фильтрует: оставляет элементы, для которых лямбда возвращает true, и не меняет их. Select проецирует: превращает каждый элемент во что-то другое, например в свойство или новый объект, сохраняя количество. Их часто объединяют в цепочку: people.Where(p => p.Age >= 18).Select(p => p.Name).
Чем First отличается от FirstOrDefault?
First() возвращает первый элемент (или первое совпадение с предикатом) и выбрасывает InvalidOperationException («Sequence contains no elements» или «no matching element»), если такого нет. FirstOrDefault() вместо этого возвращает значение типа по умолчанию: null для классов и 0 для int, поэтому проверяйте результат перед использованием.
Как отсортировать по двум полям с помощью LINQ?
Добавьте ThenBy после OrderBy: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). Два вызова OrderBy не работают: второй вызов пересортировывает всю последовательность, поэтому его ключ становится главным, а первый ключ лишь разрешает равенство, то есть наоборот по сравнению с задуманным.
Как работает GroupBy в LINQ?
GroupBy(x => key) возвращает одну группу на каждый уникальный ключ. Каждая группа это IGrouping<TKey, TElement>: у неё есть свойство Key, и она сама является последовательностью элементов с этим ключом, поэтому у неё можно вызвать Count(), Sum() или Select. Группы выходят в том порядке, в котором их ключи впервые появляются.
Что такое отложенное выполнение в LINQ?
Большинство операторов LINQ не выполняются в момент вызова; они возвращают запрос, который выполняется каждый раз при перечислении (через foreach, ToList(), Count() и так далее). Поэтому запрос видит изменения источника, сделанные после его определения, а двукратное перечисление выполняет работу дважды. Вызовите ToList() или ToArray(), чтобы выполнить запрос один раз и сохранить результаты.