Если вы пишете на C# и до сих пор обрабатываете коллекции вложенными циклами с условиями и временными списками, самое время познакомиться с LINQ. Это встроенный в язык механизм запросов, который позволяет выбирать, фильтровать, сортировать и группировать данные одним читаемым выражением. Запросы выглядят почти как SQL, но работают с обычными объектами в памяти, с базами данных и с XML.
В статье разберём основы: два вида синтаксиса, самые полезные операторы, отложенное выполнение и ловушки, в которые попадают даже опытные разработчики. Примеры максимально практичные.
Зачем нужен LINQ
Представьте задачу: есть список заказов, нужно отобрать оплаченные, отсортировать по сумме, взять пять самых крупных и вывести номера. Классический подход потребует цикла, условия, временного списка, сортировки и ещё одного цикла. LINQ решает это одной цепочкой вызовов. Код становится короче, а главное — выражает намерение, а не механику.
- Меньше шаблонного кода и временных переменных.
- Единый подход к массивам, спискам, словарям, базам и XML.
- Проверка типов на этапе компиляции.
- Лёгкая композиция: запрос можно наращивать по частям.
Два синтаксиса: запросный и методный
В LINQ есть два способа записи, и оба равноправны. Запросный синтаксис похож на SQL:
var result = from o in orders
where o.IsPaid
orderby o.Total descending
select o.Number;
Методный синтаксис использует цепочку вызовов с лямбда-выражениями:
var result = orders
.Where(o => o.IsPaid)
.OrderByDescending(o => o.Total)
.Select(o => o.Number);
Результат один и тот же. На практике методный синтаксис встречается чаще, потому что некоторые операции, например Take, Skip и Count, в запросной форме не имеют аналогов и всё равно приходится вызывать как методы. Выберите стиль, который читается лучше в вашем случае, и придерживайтесь его в проекте.
Базовые операторы, которые нужны каждый день
Where — фильтрация
Оставляет только элементы, удовлетворяющие условию. Условие записывается в виде лямбды, которая возвращает логическое значение.
var adults = users.Where(u => u.Age >= 18);
Select — проекция
Преобразует каждый элемент в нечто другое: поле, новый объект, анонимный тип.
var names = users.Select(u => u.Name);
var cards = users.Select(u => new { u.Name, u.Email });
OrderBy, ThenBy — сортировка
Сортируют по возрастанию, для убывания есть OrderByDescending и ThenByDescending.
var sorted = users.OrderBy(u => u.City).ThenBy(u => u.Name);
GroupBy — группировка
Собирает элементы по ключу и позволяет считать агрегаты.
var byCity = users
.GroupBy(u => u.City)
.Select(g => new { City = g.Key, Count = g.Count() });
Join — соединение
Объединяет две коллекции по ключу, как соединение таблиц.
var rows = orders.Join(users,
o => o.UserId,
u => u.Id,
(o, u) => new { u.Name, o.Total });
Агрегаты и проверки
Часто нужно не получить коллекцию, а ответить на вопрос о ней. Для этого есть набор операторов:
Count()— количество элементов, с условием или без;Sum(),Average(),Min(),Max()— числовые агрегаты;Any()— есть ли хоть один элемент, подходящий под условие;All()— подходят ли все элементы;First(),FirstOrDefault()— первый элемент или значение по умолчанию;Single(),SingleOrDefault()— ровно один элемент, иначе исключение;Distinct()— убрать повторы;Take()иSkip()— взять или пропустить заданное число элементов, основа постраничного вывода.
Для проверки наличия используйте Any(), а не Count() > 0. Первый вариант останавливается на первом найденном элементе, второй пересчитывает всю коллекцию.
Отложенное выполнение
Самая важная особенность LINQ, из-за которой возникает больше всего путаницы. Запрос не выполняется в момент объявления. Он выполняется только тогда, когда вы начинаете перебирать результат, например в foreach, либо вызываете метод, требующий готовых данных: ToList(), ToArray(), Count(), First().
var query = items.Where(x => x.Price > 100); // ещё ничего не выполнено
items.Add(new Item { Price = 500 });
var list = query.ToList(); // выполнение здесь, новый элемент учтён
У такого поведения есть плюсы: запрос можно собирать по частям, а вычисления проводятся только при необходимости. И есть минусы: если перебрать один и тот же запрос несколько раз, он будет выполнен несколько раз. Если источником служит база данных, это означает несколько обращений к ней. Когда результат нужен несколько раз, материализуйте его через ToList().
LINQ к базам данных
Когда источником данных служит база через ORM, например Entity Framework, LINQ-запрос транслируется в SQL. Это очень удобно, но требует внимания.
- Не все методы можно перевести в SQL. Вызов собственной функции внутри условия может вызвать ошибку или загрузку всей таблицы в память.
- Порядок операторов влияет на то, что окажется в SQL. Фильтруйте до материализации, а не после.
- Следите за количеством обращений к базе. Запрос внутри цикла по результатам другого запроса — классическая проблема «N плюс один».
- Включайте логирование сгенерированного SQL на время разработки и смотрите, что на самом деле уходит в базу.
Простой пример неправильного порядка: сначала ToList(), потом Where. Вы загружаете в память всю таблицу, а потом отбрасываете лишнее. Правильно наоборот: Where первым, ToList() в конце.
Типичные ошибки
- Многократное перечисление. Запрос выполняется при каждом обходе. Сохраняйте результат, если он нужен снова.
- Изменение коллекции во время перебора. Приводит к исключению. Сначала материализуйте, потом меняйте.
- Использование Single вместо First. Если элементов может быть больше одного,
Singleвыбросит исключение. - Обращение к результату FirstOrDefault без проверки. Для ссылочных типов он возвращает null, и следующая строка падает.
- Слишком длинные цепочки. Если запрос не помещается на экран, разбейте его на несколько именованных частей.
- Злоупотребление LINQ в горячих участках. Для критичных к скорости циклов обычный цикл иногда быстрее, потому что нет накладных расходов на делегаты.
Практический пример: отчёт по продажам
Допустим, есть список продаж с полями менеджер, категория, сумма. Нужно получить рейтинг менеджеров по выручке среди продаж выше определённого порога:
var report = sales
.Where(s => s.Amount > 1000)
.GroupBy(s => s.Manager)
.Select(g => new
{
Manager = g.Key,
Total = g.Sum(s => s.Amount),
Deals = g.Count()
})
.OrderByDescending(r => r.Total)
.Take(10)
.ToList();
Такой запрос читается как описание задачи на человеческом языке: отобрать, сгруппировать, посчитать, отсортировать, взять десять, выполнить. Без LINQ вам бы пришлось писать словарь, циклы и ручную сортировку.
Советы по читаемости и тестированию
Запросы LINQ легко тестировать: они работают с обычными коллекциями, а значит, для проверки достаточно подготовить небольшой список объектов и сравнить результат с ожидаемым. Не нужна ни база, ни окружение.
- Давайте промежуточным запросам осмысленные имена:
paidOrders,topCustomers. - Выносите повторяющиеся условия в отдельные методы или выражения.
- Не вставляйте в лямбды побочные эффекты: запись в журнал или изменение состояния внутри
Selectприводит к неожиданным результатам из-за отложенного выполнения. - Переносите каждый оператор цепочки на новую строку, так проще читать и сравнивать правки.
Заключение
LINQ — один из самых полезных инструментов C#. Он делает код короче, понятнее и безопаснее, а работа с данными становится единообразной вне зависимости от источника. Освойте базовые операторы, запомните про отложенное выполнение и следите за тем, что именно уходит в базу. После пары недель регулярного применения вы вряд ли захотите возвращаться к ручным циклам там, где достаточно одного выражения.