Перейти к основному содержимому

LINQ - язык интегрированных запросов

Разработчику Архитектору

См. также — Коллекции и структуры данных, Итераторы и ключевое слово yield, Сериализация и десериализация объектов.


LINQ

LINQ - запросы на C# к данным

LINQ позволяет писать запросы к данным на C#, без отдельного языка SQL/XML в строках. Один стиль — для списков в памяти и для таблиц в базе (через EF Core).

Маршрут:

  1. LINQ к коллекциям (IEnumerable) — эта статья.
  2. LINQ к БД (IQueryable, EF) — работа с БД, EF Core — первая программа.
  3. Шпаргалка операторов — справочник LINQ.

Курс Microsoft: LINQ в C#.


Что такое LINQ?

LINQ (Language Integrated Query) — набор технологий, встроенных в C# — запрос становится конструкцией языка первого класса, наравне с классами, методами и событиями. Традиционные запросы к данным часто оформлялись как строки без проверки типов на этапе компиляции и без IntelliSense; для каждого формата (SQL, XML, веб-сервисы) требовался отдельный язык запросов. LINQ даёт единую модель C# для коллекций в памяти, XML, реляционных БД и других источников при наличии поставщика LINQ.

В запросе LINQ вы работаете с объектами C#; провайдер (например, EF Core) переводит выражение в SQL или другой язык целевой системы. Синтаксис запросов и методов семантически эквивалентен; компилятор преобразует выражения запроса в вызовы стандартных операторов по спецификации C#.

Полный учебный цикл Microsoft: LINQ в C#.

Минимальный пример (источник → запрос → выполнение в foreach):

int[] scores = [97, 92, 81, 60];

IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;

foreach (var i in scoreQuery)
Console.Write($"{i} "); // 97 92 81

Разбор:

  • Фрагмент начинается с int[] scores = [97, 92, 81, 60]; и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: IEnumerable<T> задаёт контракт перечисления и поддерживает ленивую обработку последовательности. Условие where в LINQ-фрагменте фильтрует исходную последовательность по заданному предикату. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

Для компиляции нужен using System.Linq; (в .NET 8+ часто подключается через implicit usings).


Три части операции запроса

Любой LINQ-запрос состоит из трёх шагов (общие сведения о запросах):

  1. Источник данных — объект, поддерживающий IEnumerable<T> или производный IQueryable<T> (запрашиваемый тип).
  2. Создание запроса — переменная хранит описание выборки, а не готовые данные.
  3. Выполнениеforeach, ToList(), Count() и т.д.; только здесь извлекаются элементы.
int[] numbers = [0, 1, 2, 3, 4, 5, 6];

// 1. Источник: numbers
// 2. Запрос (ещё не выполнен)
var numQuery = from num in numbers
where num % 2 == 0
select num;

// 3. Выполнение
foreach (int num in numQuery)
Console.Write($"{num} "); // 0 2 4 6

Разбор:

  • Фрагмент начинается с int[] numbers = [0, 1, 2, 3, 4, 5, 6]; и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Условие where в LINQ-фрагменте фильтрует исходную последовательность по заданному предикату. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

Переменная запроса можно переиспользовать: при каждом перечислении читается актуальное состояние источника (важно для БД и изменяемых коллекций).


Запрашиваемые типы

Запрашиваемый тип — любой тип, реализующий IEnumerable<T> или унаследованный от него интерфейс, обычно IQueryable<T>. Источник не обязан быть специально "под LINQ": достаточно перечисления.

ИсточникПример
Массив, список, словарьint[], List<T>, Dictionary<,>
LINQ to XMLXElement.Load(...) → запросы к XElement — см. XML в .NET
EF CoreDbSet<T>IQueryable<T>
Неуниверсальные коллекцииArrayList через Cast<T>() / запросы к коллекциям

Если данные ещё не в памяти как запрашиваемый тип, поставщик материализует или строит обёртку (XML в XElement, таблицы — в сущности EF).


Два стиля записи запросов

В LINQ существует два эквивалентных способа формулировки запросов: синтаксис запросов (Query Syntax) и синтаксис методов (Method Syntax). Оба стиля транслируются компилятором в один и тот же промежуточный код и дают идентичный результат. Выбор между ними — вопрос предпочтений и читаемости в конкретном контексте.


Синтаксис запросов

Синтаксис запросов напоминает SQL. Он использует ключевые слова, такие как from, where, select, orderby, group, join. Этот стиль особенно удобен при сложных запросах с несколькими источниками данных, соединениями или группировками. Пример:

var result = from student in students
where student.Age > 18
orderby student.Name
select student;

Разбор:

  • Фрагмент начинается с var result = from student in students и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Условие where в LINQ-фрагменте фильтрует исходную последовательность по заданному предикату. Сортировка (OrderBy/orderby) задаёт порядок элементов в результирующей последовательности.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

Этот код читается почти как естественный язык — "из коллекции студентов взять тех, чей возраст больше восемнадцати, упорядочить по имени и вернуть их". Компилятор преобразует такой запрос в цепочку вызовов методов расширения, что делает его полностью совместимым с синтаксисом методов.


Синтаксис методов

Синтаксис методов основан на использовании методов расширения, определённых в классе System.Linq.Enumerable (для IEnumerable<T>) и System.Linq.Queryable (для IQueryable<T>). Эти методы принимают делегаты или лямбда-выражения, описывающие логику фильтрации, проекции или сортировки. Пример:

var result = students
.Where(s => s.Age > 18)
.OrderBy(s => s.Name);

Разбор:

  • Фрагмент начинается с var result = students и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Сортировка (OrderBy/orderby) задаёт порядок элементов в результирующей последовательности. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Этот стиль ближе к функциональному программированию. Он особенно удобен при простых запросах, когда требуется применить одну или две операции. Многие разработчики предпочитают его за лаконичность и прямую связь с API .NET.

Оба стиля могут сочетаться в одном выражении. Например, можно начать с синтаксиса запросов, а затем продолжить цепочкой методов.

Операторы вроде Count, Max, Min, Average, Sum, First не имеют отдельных предложений в синтаксисе запроса — их вызывают как методы (часто в конце цепочки, потому что возвращают скаляр, а не последовательность):

var numCount = (
from num in numbers
where num is > 3 and < 7
select num
).Count();

// или эквивалентно
var numCount2 = numbers.Count(n => n is > 3 and < 7);

Разбор:

  • Фрагмент начинается с var numCount = ( и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Условие where в LINQ-фрагменте фильтрует исходную последовательность по заданному предикату. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

Стандартные операторы — методы расширения в System.Linq.Enumerable / Queryable; в область видимости их вводит using System.Linq. Подробнее: написание запросов LINQ, обзор стандартных операторов.


Ключевые слова синтаксиса запроса

Ключевое словоНазначение
fromИсточник и переменная диапазона (from x in source)
whereФильтр
selectПроекция результата
orderby / ascending / descendingСортировка
group / byГруппировка
join / on / equalsСоединение
intoПродолжение запроса с промежуточной последовательностью
letПромежуточная переменная в запросе

Полный список: ключевые слова запросов (LINQ).


Связи типов в операции запроса

Операции LINQ строго типизированы: тип элементов источника, тип переменной запроса и тип переменной в foreach должны быть согласованы; ошибки ловятся на этапе компиляции (связи типов).

  • Тип аргумента источника (List<Customer>, int[]) задаёт тип переменной диапазона в from cust in customers.
  • Предложение select задаёт тип элементов результата: select custIEnumerable<Customer>, select cust.NameIEnumerable<string>.
IEnumerable<Customer> customerQuery =
from cust in customers
where cust.City == "London"
select cust;

foreach (Customer customer in customerQuery)
Console.WriteLine(customer.LastName);

// Эквивалент с var — компилятор выводит те же типы
var customerQuery2 =
from cust in customers
where cust.City == "London"
select cust;

Разбор:

  • Фрагмент начинается с IEnumerable<Customer> customerQuery = и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: IEnumerable<T> задаёт контракт перечисления и поддерживает ленивую обработку последовательности. Условие where в LINQ-фрагменте фильтрует исходную последовательность по заданному предикату. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

LINQ опирается на обобщённые типы (List<T>, IEnumerable<T>). Переменные запроса к коллекциям в памяти обычно имеют вид IEnumerable<T> или IQueryable<T>.


Основные операции LINQ

LINQ предоставляет богатый набор стандартных операторов запросов, которые позволяют выполнять все типичные действия с данными. Эти операторы реализованы как методы расширения и работают с любыми типами, реализующими интерфейс IEnumerable<T> или IQueryable<T>.

Отложенное выполнение: цепочки вроде Where(...).Select(...) не выполняются сразу — они строят описание запроса. Материализация происходит при foreach, ToList(), ToArray(), Count() и т.д. Повторный foreach по тому же IEnumerable без материализации может заново выполнить всю цепочку (важно для I/O и итераторов с yield).


Фильтрация

Фильтрация — это выбор элементов, удовлетворяющих заданному условию. Основной оператор — Where. Он принимает предикат (функцию, возвращающую bool) и возвращает новую последовательность, содержащую только те элементы, для которых предикат вернул true.

Пример:

var adults = people.Where(p => p.Age >= 18);

Разбор:

  • Фрагмент начинается с var adults = people.Where(p => p.Age >= 18); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Проекция

Проекция — это преобразование каждого элемента исходной последовательности в новый вид. Оператор Select позволяет создавать новые объекты, извлекать отдельные свойства или комбинировать данные.

Пример:

var names = students.Select(s => s.Name);
var nameAgePairs = students.Select(s => new { s.Name, s.Age });

Разбор:

  • Фрагмент начинается с var names = students.Select(s => s.Name); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Select(...) выполняет проекцию и формирует новый результат для каждого элемента. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки. Оператор new создаёт экземпляры объектов и коллекций, формируя рабочее состояние примера.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Проекция может быть скалярной (возврат одного значения) или составной (возврат анонимного типа или нового объекта).


Упорядочение

Упорядочение обеспечивает сортировку элементов по одному или нескольким критериям. Основные методы — OrderBy, OrderByDescending, ThenBy, ThenByDescending. Они поддерживают цепочку сортировок: сначала по основному полю, затем по второстепенному.

Пример:

var sorted = products
.OrderBy(p => p.Category)
.ThenBy(p => p.Price);

Разбор:

  • Фрагмент начинается с var sorted = products и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Сортировка (OrderBy/orderby) задаёт порядок элементов в результирующей последовательности. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Сортировка стабильна: порядок равных элементов сохраняется.


Агрегация

Агрегирующие операторы сводят всю последовательность к одному значению. К ним относятся:

  • Count — количество элементов;
  • Sum — сумма числовых значений;
  • Average — среднее арифметическое;
  • Min, Max — минимальное и максимальное значение.

Эти методы выполняются немедленно и возвращают скалярный результат, а не последовательность.

Пример:

int totalStudents = students.Count();
double avgGrade = grades.Average();
decimal totalRevenue = orders.Sum(o => o.Amount);

Разбор:

  • Фрагмент начинается с int totalStudents = students.Count(); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы — Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • Инструкции выполняются последовательно: объявление сущностей, вызов операций и получение конечного результата.

Квантификаторы

Квантификаторы проверяют наличие или универсальность условия в последовательности:

  • Any — возвращает true, если хотя бы один элемент удовлетворяет условию;
  • All — возвращает true, если все элементы удовлетворяют условию;
  • Contains — проверяет, присутствует ли заданный элемент в последовательности.

Эти методы часто используются в условиях валидации или принятия решений.

Пример:

bool hasMinors = students.Any(s => s.Age < 18);
bool allPassed = exams.All(e => e.Score >= 50);
bool knownUser = validLogins.Contains(inputLogin);

Разбор:

  • Фрагмент начинается с bool hasMinors = students.Any(s => s.Age < 18); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы — Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • Инструкции выполняются последовательно: объявление сущностей, вызов операций и получение конечного результата.

Отложенное выполнение (Deferred Execution)

Одна из ключевых особенностей LINQ — отложенное выполнение. Запрос не выполняется в момент его определения, а только тогда, когда результаты действительно потребуются. Это означает, что переменная, содержащая LINQ-выражение, хранит не данные, а инструкцию по их получению.

Пример:

var query = students.Where(s => s.Grade > 80); // запрос не выполнен
// ... время проходит, коллекция students может измениться ...
foreach (var student in query) // здесь происходит выполнение
{
Console.WriteLine(student.Name);
}

Разбор:

  • Фрагмент начинается с var query = students.Where(s => s.Grade > 80); // запрос не выполнен и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Благодаря отложенному выполнению, LINQ-запросы могут быть динамическими: они всегда работают с актуальным состоянием источника данных. Однако это также требует осторожности: если источник изменяется между определением запроса и его перечислением, результат может отличаться от ожидаемого.

Некоторые операторы, такие как Count, ToList, ToArray, First, Single, принудительно выполняют запрос. Они называются немедленными (eager), потому что возвращают конкретное значение, а не отложенную последовательность.


Классификация выполнения операторов

По способу выполнения стандартные операторы делятся на несколько категорий (классификация в документации):

КатегорияПоведениеПримеры
НемедленноеЧитает источник и завершает операцию; скаляр или материализованная коллекцияCount, Sum, Average, Min, Max, First, ToList, ToArray
Отложенное, потоковоеОбрабатывает элементы по мере чтения, без полного прохода по источнику заранееWhere, Select, Take, Skip, TakeWhile, SkipWhile, Distinct
Отложенное, непотоковоеСначала читает все нужные данные (буфер/сортировка), затем отдаёт результатOrderBy, GroupBy, Join, Reverse

ToList() / ToArray() кэшируют результат одного выполнения — удобно, если одни и те же данные нужны многократно без повторного обхода источника.

LINQ to Objects — запросы к любому IEnumerable<T> в памяти (LINQ to Objects): декларативный код переносится на другие провайдеры без смены шаблона from / Where / Select.


IEnumerable<T> и IQueryable<T>

LINQ работает с двумя основными интерфейсами: IEnumerable<T> и IQueryable<T>. Хотя они внешне похожи, их поведение принципиально различается.


IEnumerable<T>

Этот интерфейс представляет последовательность объектов в памяти. Все операторы LINQ, применённые к IEnumerable<T>, выполняются на стороне клиента — то есть в самой программе на C#. Данные уже загружены, и запросы работают с ними как с обычными коллекциями.

Пример — работа с List<T>, массивами, результатами File.ReadAllLines().


IQueryable<T>

Этот интерфейс предназначен для работы с удалёнными источниками данных, такими как реляционные базы данных. Он содержит не только последовательность, но и дерево выражения (Expression<Func<...>>), описывающее логику запроса. Когда запрос выполняется, провайдер LINQ (например, Entity Framework Core) анализирует это дерево и транслирует его в язык целевой системы — чаще всего в SQL.

Пример:

var query = dbContext.Students
.Where(s => s.Age > 18)
.Select(s => s.Name);

Разбор:

  • Фрагмент начинается с var query = dbContext.Students и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Select(...) выполняет проекцию и формирует новый результат для каждого элемента. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Здесь dbContext.Students имеет тип IQueryable<Student>. При перечислении query EF Core сгенерирует SQL-запрос вида:

SELECT Name FROM Students WHERE Age > 18

Разбор:

  • Фрагмент начинается с SELECT Name FROM Students WHERE Age > 18 и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Основной акцент здесь на типах и сигнатурах: компилятор заранее проверяет корректность использования конструкций.
  • Инструкции выполняются последовательно: объявление сущностей, вызов операций и получение конечного результата.

и выполнит его на сервере базы данных. Только имена студентов будут переданы клиенту, а не вся таблица.

Это даёт огромное преимущество в производительности — фильтрация, сортировка и проекция происходят на стороне базы данных, минимизируя объём передаваемых данных.


Различия в поведении

  • Для IEnumerable<T> лямбда-выражения компилируются в делегаты (Func<T, bool>).
  • Для IQueryable<T> лямбда-выражения сохраняются как деревья выражений (Expression<Func<T, bool>>), чтобы их можно было проанализировать и преобразовать.
  • Некоторые операции, допустимые в памяти (например, вызов произвольного метода C# внутри Where), не могут быть переведены в SQL и вызовут ошибку при работе с IQueryable<T>.

Entity Framework Core активно использует IQueryable<T>, чтобы обеспечить эффективное взаимодействие с базой данных. Понимание разницы между IEnumerable<T> и IQueryable<T> критически важно для написания производительных приложений.

LINQ и массовая загрузка — разные задачи. IQueryable хорошо переводит фильтрацию и проекцию в один SQL-запрос; цикл foreach + Add + SaveChanges на десятках тысяч строк — это тысячи round-trip, не "пакетный LINQ". Для импорта используют bulk (SqlBulkCopy, staging + MERGE, ExecuteUpdate, сторонние провайдеры) и чанки с checkpoint — Пакетная работа с данными, ORM на практике.

  • Запросы к IEnumerable<T> компилятор обычно превращает в делегаты (Func<...>).
  • Запросы к IQueryable<T> — в деревья выражений для анализа провайдером (деревья выражений).

Поставщики IQueryable

Поставщик реализует IQueryable<T> и IQueryProvider. Сложность и возможности сильно различаются (как включить LINQ к источнику):

УровеньХарактеристикаПример
ПростойОдин внешний API, узкий набор операций; часть логики — локально через EnumerableОбёртка над одной веб-службой
СреднийЧастично выразительный язык запросов целевой системы, фиксированная модель типовНесколько методов REST с выбором по дереву выражения
СложныйПолный перевод LINQ (часто в SQL), открытая система типов, маппинг пользовательских типовEntity Framework Core

Для данных в памяти: реализуйте IEnumerable<T> или добавьте стандартные операторы запроса как методы расширения с отложенным выполнением (yield return).

Для удалённых данных: предпочтительна реализация IQueryable<T> и трансляция дерева выражений.


Группировка и соединения

LINQ поддерживает сложные операции, такие как группировка и соединение нескольких последовательностей, что делает его мощным инструментом для анализа связанных данных.


Группировка

Оператор GroupBy позволяет разбить последовательность на подгруппы по заданному ключу. Каждая группа представляет собой объект, содержащий общий ключ и коллекцию элементов, соответствующих этому ключу.

Пример:

var studentsByGrade = students.GroupBy(s => s.GradeLevel);

Разбор:

  • Фрагмент начинается с var studentsByGrade = students.GroupBy(s => s.GradeLevel); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: GroupBy(...) группирует данные по ключу для последующей агрегации или отчётов. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Результат — последовательность групп, где каждая группа содержит всех студентов одного уровня обучения. Можно дополнительно применять агрегацию внутри групп:

var avgScoreBySubject = exams
.GroupBy(e => e.Subject)
.Select(g => new { Subject = g.Key, Average = g.Average(e => e.Score) });

Разбор:

  • Фрагмент начинается с var avgScoreBySubject = exams и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Select(...) выполняет проекцию и формирует новый результат для каждого элемента. GroupBy(...) группирует данные по ключу для последующей агрегации или отчётов. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Этот запрос вычисляет средний балл по каждому предмету. Группировка особенно полезна при подготовке отчётов, статистики или сводных таблиц.


Соединения

LINQ поддерживает несколько типов соединений, аналогичных SQL:

  • Inner Join (Join) — возвращает пары элементов, для которых найдено соответствие в обеих последовательностях.
  • Group Join — объединяет каждый элемент первой последовательности с коллекцией соответствующих элементов из второй.
  • Left Outer Join — реализуется через GroupJoin и DefaultIfEmpty, чтобы сохранить элементы из левой последовательности даже при отсутствии совпадений.

Пример внутреннего соединения:

var ordersWithCustomers = from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new { order.Id, customer.Name, order.Total };

Разбор:

  • Фрагмент начинается с var ordersWithCustomers = from order in orders и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Join связывает элементы двух источников по совпадающему ключу. Оператор new создаёт экземпляры объектов и коллекций, формируя рабочее состояние примера.
  • Запрос сначала описывается, а выполняется при перечислении (foreach) или терминальной операции (ToList, Count, First).

Соединения позволяют эффективно работать с нормализованными данными, например, связывать заказы с клиентами, товары с категориями или сотрудников с отделами.


Обработка ошибок и пустых результатов

LINQ предоставляет методы, которые помогают безопасно работать с потенциально пустыми последовательностями:

  • FirstOrDefault() — возвращает первый элемент или значение по умолчанию (null для ссылочных типов, 0 для чисел), если последовательность пуста.
  • SingleOrDefault() — возвращает единственный элемент, если он существует и уникален; иначе выбрасывает исключение или возвращает значение по умолчанию при использовании SingleOrDefault().
  • DefaultIfEmpty() — заменяет пустую последовательность одним элементом по умолчанию.

Эти методы предотвращают исключения InvalidOperationException, которые могут возникнуть при вызове First() или Single() на пустой коллекции.

Пример:

var topStudent = students
.Where(s => s.Grade > 95)
.OrderByDescending(s => s.Grade)
.FirstOrDefault();

if (topStudent != null)
{
Console.WriteLine($"Лучший студент: {topStudent.Name}");
}

Разбор:

  • Фрагмент начинается с var topStudent = students и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Производительность и оптимизация

Хотя LINQ упрощает написание кода, важно понимать его влияние на производительность:

  • Каждый оператор LINQ создаёт промежуточный объект (например, WhereEnumerableIterator), что может привести к накладным расходам при длинных цепочках.
  • Отложенное выполнение означает, что запрос может выполняться многократно, если к нему обращаются несколько раз. Чтобы избежать этого, следует материализовать результат с помощью ToList() или ToArray(), если он используется повторно.
  • При работе с базами данных через Entity Framework Core важно следить за тем, чтобы запросы транслировались в эффективный SQL. Избегайте вызовов методов C# внутри Where или Select, которые не могут быть переведены в SQL — это приведёт к загрузке всех данных в память и фильтрации на стороне клиента.

Пример плохой практики:

// ❌ Может привести к загрузке всей таблицы в память
var result = dbContext.Products
.Where(p => IsSpecialCategory(p.Category)) // IsSpecialCategory — метод C#
.ToList();

Разбор:

  • Фрагмент начинается с // ❌ Может привести к загрузке всей таблицы в память и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Правильный подход — использовать только выражения, поддерживаемые провайдером:

// ✅ Переводится в SQL
var result = dbContext.Products
.Where(p => p.Category == "Electronics" || p.Category == "Books")
.ToList();

Разбор:

  • Фрагмент начинается с // ✅ Переводится в SQL и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Расширяемость LINQ

LINQ можно расширять, создавая собственные методы расширения. Это позволяет инкапсулировать часто используемую логику в читаемые и переиспользуемые компоненты.

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • Фрагмент начинается с public static class LinqExtensions и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: IEnumerable<T> задаёт контракт перечисления и поддерживает ленивую обработку последовательности. yield return выдаёт элементы по одному и сохраняет состояние итератора между шагами. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current.
  • Поток выполнения ленивый: метод продолжает выполнение только при запросе следующего элемента, а не целиком за один вызов.

Такие расширения делают код более выразительным и близким к предметной области.


LINQ в реальных сценариях

LINQ активно применяется в повседневной разработке:

  • Фильтрация пользовательских данных в веб-приложениях (например, поиск по каталогу товаров).
  • Преобразование API-ответов из JSON в удобные DTO-объекты.
  • Анализ логов — группировка событий по времени, уровню серьёзности или источнику.
  • Подготовка данных для отчётов — агрегация, сортировка, проекция.
  • Валидация коллекций — проверка условий с помощью All, Any.

Благодаря своей декларативной природе, LINQ делает код более читаемым и менее подверженным ошибкам, связанным с ручным управлением циклами и условиями.


Внутреннее устройство — как работает LINQ

Чтобы глубже понять поведение LINQ, полезно заглянуть под капот. Основа LINQ — это интерфейсы IEnumerable<T> и IQueryable<T>, а также механизм итераторов и деревьев выражений.


Итераторы и yield return

Методы расширения в System.Linq.Enumerable реализованы с использованием ключевого слова yield return. Это позволяет создавать последовательности "по требованию", без предварительного выделения памяти под весь результат. Например, метод Where не создаёт новый список, а возвращает объект, который при перечислении проходит по исходной коллекции и возвращает элементы, удовлетворяющие условию.

Такой подход обеспечивает:

  • Экономию памяти — данные не копируются до тех пор, пока это не требуется.
  • Ленивую обработку — элементы обрабатываются только тогда, когда к ним обращаются.
  • Композицию — цепочка операторов (Where().Select().OrderBy()) строится как последовательность итераторов, каждый из которых оборачивает предыдущий.

Это и есть суть отложенного выполнения: сам запрос — это конвейер обработки, а не готовый результат.


Деревья выражений

При работе с IQueryable<T> лямбда-выражения не компилируются в исполняемый код, а сохраняются в виде деревьев выражений — структуры данных, описывающей операции, переменные и вызовы. Провайдер LINQ (например, Entity Framework Core) анализирует это дерево и преобразует его в команду на другом языке — чаще всего SQL.

Пример дерева:

Expression<Func<Student, bool>> expr = s => s.Age > 18;

Разбор:

  • Фрагмент начинается с Expression<Func<Student, bool>> expr = s => s.Age > 18; и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы — Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • Инструкции выполняются последовательно: объявление сущностей, вызов операций и получение конечного результата.

Здесь expr содержит не функцию, а описание: "сравнить свойство Age объекта типа Student с числом 18". Это описание можно прочитать, модифицировать или перевести.

Благодаря деревьям выражений становится возможным:

  • Построение динамических запросов.
  • Создание универсальных фильтров и поисковых систем.
  • Интеграция с внешними системами, которые не понимают C#, но понимают SQL, XPath или другие языки запросов.

Сравнение с традиционными циклами

Раньше, до LINQ, для фильтрации списка студентов старше 18 лет пришлось бы писать:

var adults = new List<Student>();
foreach (var student in students)
{
if (student.Age > 18)
{
adults.Add(student);
}
}

Разбор:

  • Фрагмент начинается с var adults = new List<Student>(); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: List<T> используется как типобезопасная динамическая коллекция с доступом по индексу. foreach перечисляет элементы через GetEnumerator(), MoveNext() и Current. Оператор new создаёт экземпляры объектов и коллекций, формируя рабочее состояние примера.
  • Выполнение идёт поэлементно: на каждом шаге цикла берётся текущий элемент и применяется нужное действие.

LINQ заменяет этот шаблон одной строкой:

var adults = students.Where(s => s.Age > 18);

Разбор:

  • Фрагмент начинается с var adults = students.Where(s => s.Age > 18); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Преимущества:

  • Краткость и выразительность.
  • Отсутствие ручного управления коллекцией.
  • Возможность легко комбинировать операции.
  • Уменьшение количества ошибок (например, забыть создать список или добавить элемент).

LINQ не заменяет циклы полностью — в случаях, где требуется сложная логика с побочными эффектами, foreach остаётся уместным. Но для чистых операций над данными LINQ — предпочтительный выбор.


Поддержка в других языках и платформах

Хотя LINQ родился в экосистеме .NET, его идеи повлияли на другие языки:

  • Java имеет Stream API, во многом вдохновлённый LINQ.
  • JavaScript использует методы вроде filter, map, reduce — аналоги Where, Select, агрегации.
  • Python предлагает генераторы списков и функции filter, map.

Однако ни один из этих аналогов не достигает уровня интеграции LINQ с языком и компилятором. Только в C# запросы являются частью синтаксиса, проверяются на этапе компиляции и поддерживают два равноценных стиля записи.


Типичные ошибки и как их избежать

  1. Многократное выполнение отложенного запроса
    Если использовать один и тот же LINQ-запрос несколько раз без материализации, он будет выполняться заново каждый раз. Это может привести к несогласованности данных или снижению производительности.
    Решение: вызвать ToList() или ToArray(), если результат используется многократно.

  2. Смешивание IEnumerable<T> и IQueryable<T>
    Преобразование IQueryable<T> в IEnumerable<T> (например, через .AsEnumerable()) заставляет последующие операции выполняться в памяти, а не в базе данных. Это может привести к загрузке лишних данных.
    Решение: выполнять фильтрацию и проекцию на стороне базы данных, пока это возможно.

  3. Использование не транслируемых методов в EF Core
    Вызов произвольного метода C# внутри Where при работе с IQueryable<T> вызовет исключение или неэффективную загрузку всех данных.
    Решение: ограничиваться выражениями, поддерживаемыми провайдером.

  4. Игнорирование null-значений
    При работе с проекциями или соединениями важно учитывать, что некоторые свойства могут быть null.
    Решение: использовать безопасную навигацию (?.) или проверки перед доступом.


Продвинутые сценарии

Динамические запросы

Иногда условия фильтрации неизвестны на этапе компиляции — например, пользователь выбирает критерии в интерфейсе. В таких случаях можно строить выражения программно с помощью класса Expression.

Пример:

var param = Expression.Parameter(typeof(Student), "s");
var body = Expression.GreaterThan(
Expression.Property(param, "Age"),
Expression.Constant(18)
);
var lambda = Expression.Lambda<Func<Student, bool>>(body, param);
var query = students.Where(lambda);

Разбор:

  • Фрагмент начинается с var param = Expression.Parameter(typeof(Student), "s"); и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

Это позволяет создавать гибкие системы фильтрации без жёсткой привязки к конкретным условиям.


Параллельные запросы (PLINQ)

Для обработки больших объёмов данных в памяти можно использовать Parallel LINQ (PLINQ). Он автоматически распределяет работу между потоками:

var result = data.AsParallel()
.Where(x => x.IsValid)
.Select(x => Process(x))
.ToArray();

Разбор:

  • Фрагмент начинается с var result = data.AsParallel() и демонстрирует практический паттерн, который используется в реальном C#-коде.
  • Ключевые элементы: Where(...) фильтрует элементы по условию. Select(...) выполняет проекцию и формирует новый результат для каждого элемента. Лямбда => задаёт компактную функцию для фильтрации, сортировки, проекции или проверки.
  • LINQ-цепочка выполняется последовательно как конвейер преобразований — фильтрация, проекция, сортировка и материализация.

PLINQ полезен при CPU-интенсивных операциях, но требует осторожности: не все операции потокобезопасны, а накладные расходы на параллелизм могут перевесить выгоду при малых данных.


Справочник API

Полный перечень операторов Enumerable и Queryable, сигнатуры, PLINQ, IAsyncEnumerable, отложенное vs немедленное выполнение и типичные ловушки — в Справочнике по LINQ.

Класс / интерфейсРоль
EnumerableLINQ to Objects: делегаты Func<...>, выполнение в памяти
QueryableУдалённые источники: Expression<Func<...>>, трансляция провайдером
ParallelEnumerablePLINQ для CPU-bound обработки больших коллекций
IEnumerable<T>, IQueryable<T>Контракты последовательности и отложенного запроса
IGrouping, ILookupРезультаты GroupBy и ToLookup

Чеклист перед применением LINQ к БД

Чтобы не получить странный SQL и лишние данные:

  • держите запрос в IQueryable<T> как можно дольше;
  • не вызывайте ToList() раньше времени;
  • следите, что выражения переводимы провайдером;
  • проверяйте SQL-лог в важных местах (фильтры, пагинация, join).

Типичные анти-паттерны

  • Цепочка из 8–10 операторов без промежуточных именованных шагов — ухудшается читаемость.
  • Многократное перечисление одного и того же IEnumerable<T> с дорогим источником.
  • Использование LINQ там, где нужен явный императивный алгоритм с побочными эффектами.

Хорошее правило: если выражение тяжело прочитать вслух за 10–15 секунд, разбейте его на пару шагов с промежуточными переменными.


Что попробовать

  1. Перепишите один запрос в двух стилях — query syntax и method syntax.
  2. Сравните Where на List<T> и на DbSet<T> — во втором случае смотрите SQL в логе EF (Entity Framework Core — первая программа).
  3. Откройте справочник LINQ под свою задачу (join, группировка, агрегат).

Дополнительные материалы (Microsoft Learn)

ТемаСсылка
Обзор LINQИнтегрированный язык запросов (LINQ)
Три части запроса, отложенное/немедленное выполнениеОбщие сведения о запросах LINQ
Синтаксис запросов и методов, смешанный стильНаписание запросов LINQ
Типы переменных в запросеСвязи типов в операциях запросов LINQ
Справочник операторовСтандартные операторы запроса
Пошаговое руководствоНаписание запросов LINQ (walkthrough)
Шпаргалка в энциклопедииСправочник по LINQ