«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » .NET и C# » LINQ: запросы к данным прямо в коде

LINQ: запросы к данным прямо в коде

Если вы пишете на 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() в конце.

Типичные ошибки

  1. Многократное перечисление. Запрос выполняется при каждом обходе. Сохраняйте результат, если он нужен снова.
  2. Изменение коллекции во время перебора. Приводит к исключению. Сначала материализуйте, потом меняйте.
  3. Использование Single вместо First. Если элементов может быть больше одного, Single выбросит исключение.
  4. Обращение к результату FirstOrDefault без проверки. Для ссылочных типов он возвращает null, и следующая строка падает.
  5. Слишком длинные цепочки. Если запрос не помещается на экран, разбейте его на несколько именованных частей.
  6. Злоупотребление 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#. Он делает код короче, понятнее и безопаснее, а работа с данными становится единообразной вне зависимости от источника. Освойте базовые операторы, запомните про отложенное выполнение и следите за тем, что именно уходит в базу. После пары недель регулярного применения вы вряд ли захотите возвращаться к ручным циклам там, где достаточно одного выражения.

Р
Редакция
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».