LINQ: основные операторы
↑ 1.5 Коллекции и основы LINQ · ← Предыдущая · → Следующая
Зачем это на собесе
LINQ пишут ежедневно: фильтрация, группировка, агрегация данных для отчётов и API. На собесе дают коллекцию и просят написать запрос, и следят за эффективностью (N·M, лишние перечисления).
Объяснение
LINQ — набор методов-расширений над IEnumerable<T> (и IQueryable<T>). Два синтаксиса: методы (используют чаще) и query syntax (как SQL).
Основные операторы
var patients = GetPatients();
// фильтрация и проекция
var names = patients.Where(p => p.Age >= 18).Select(p => $"{p.LastName} {p.FirstName}");
// сортировка
var sorted = patients.OrderBy(p => p.LastName).ThenByDescending(p => p.BirthDate);
// элементы
patients.First(p => p.Id == id); // исключение, если нет
patients.FirstOrDefault(p => p.Id == id); // null/default
patients.Single(p => p.Snils == snils); // исключение, если 0 или больше 1 — проверка уникальности
patients.Last(); patients.ElementAt(3);
// кванторы
patients.Any(p => p.IsDeleted); // есть ли хоть один — O(1)–O(n), лучше чем Count() > 0
patients.All(p => p.Age > 0);
patients.Contains(p);
// агрегаты
patients.Count(p => p.City == "Минск");
patients.Sum(p => p.Debt); patients.Average(p => p.Age); patients.Max(p => p.BirthDate);
patients.MaxBy(p => p.Age); // .NET 6: сам элемент, а не значение
patients.Aggregate(0m, (acc, p) => acc + p.Debt);
// группировка
var byCity = patients.GroupBy(p => p.City)
.Select(g => new { City = g.Key, Count = g.Count(), AvgAge = g.Average(p => p.Age) });
var countByDay = visits.CountBy(v => v.Date); // .NET 9
// соединения
var q = from e in encounters
join p in patients on e.PatientId equals p.Id
select new { p.LastName, e.Date };
var left = encounters.LeftJoin(patients, e => e.PatientId, p => p.Id, (e, p) => new { e, p }); // .NET 10; раньше GroupJoin + SelectMany + DefaultIfEmpty
// разворачивание
var allDiagnoses = encounters.SelectMany(e => e.Diagnoses);
// множества и уникальность
ids.Distinct(); patients.DistinctBy(p => p.Snils); a.Union(b); a.Intersect(b); a.Except(b);
// страницы и порции
patients.Skip(20).Take(10);
patients.Chunk(500); // батчи по 500 (для вставки в ClickHouse)
// преобразование
patients.ToList(); patients.ToArray(); patients.ToDictionary(p => p.Id); patients.ToLookup(p => p.City);Query syntax
var result = from p in patients
where p.Age >= 18
orderby p.LastName
group p by p.City into g
select new { City = g.Key, Count = g.Count() };Удобен для сложных join и let. Компилятор превращает его в вызовы методов.
Ленивые и немедленные операторы
- Ленивые (отложенное выполнение):
Where,Select,OrderBy,GroupBy,Skip,Take— строят конвейер. - Немедленные:
ToList,ToArray,ToDictionary,Count,Sum,First,Any,Max— запускают перечисление.
Нюансы и подводные камни
Count() > 0вместоAny()— перебирает всё.Any()останавливается на первом.Where(...).First()=First(predicate)— пишите короче.FirstvsSingle:Singleпроверяет уникальность и перебирает дальше. Используйте его, когда дубликат — ошибка данных.- Вложенный
WhereилиContainsпо списку внутриSelect— O(n·m). СтроитеDictionaryилиToLookupзаранее. OrderByстабилен, но O(n log n). Для топ-K без полной сортировки естьMinBy/MaxByилиPriorityQueue.- В EF Core не все методы транслируются в SQL: при клиентской оценке EF бросает исключение, а не молча грузит таблицу (с EF Core 3).
- Замыкание на переменную цикла или изменяемое поле в отложенном запросе даст значение на момент выполнения, а не создания.
Тестирование
[Fact]
public void Groups_patients_by_city()
{
var patients = new[] { P("Минск"), P("Минск"), P("Гродно") };
var result = patients.GroupBy(p => p.City).ToDictionary(g => g.Key, g => g.Count());
result.Should().BeEquivalentTo(new Dictionary<string, int> { ["Минск"] = 2, ["Гродно"] = 1 });
}Вопросы с ответами
Чем First отличается от Single и FirstOrDefault?
First возвращает первый подходящий или бросает исключение, если нет ни одного. FirstOrDefault вместо исключения возвращает default. Single требует ровно один элемент, иначе исключение (ни одного или несколько).
Что такое отложенное выполнение в LINQ?
Операторы вроде Where и Select не выполняются сразу, а строят конвейер. Он запускается при перечислении (foreach, ToList, Count), причём при каждом перечислении заново.
Как сделать left join в LINQ?
GroupJoin + SelectMany + DefaultIfEmpty (или в query syntax: join … into g from x in g.DefaultIfEmpty()). В .NET 10 появился метод LeftJoin.
Как эффективно найти для каждого заказа его клиента в двух больших списках?
Построить словарь клиентов по Id (ToDictionary) и искать за O(1), либо Join, который внутри использует хэш-таблицу. Не использовать First внутри Select — это O(n·m).
Чем GroupBy отличается от ToLookup?
GroupBy отложенный и вычисляется при перечислении. ToLookup немедленно строит неизменяемую структуру «ключ → набор элементов» с быстрым доступом по ключу.
Связанные темы
- Предыдущая: Generics: обобщённые типы и методы · Следующая: Сложность операций с коллекциями и выбор структуры
- LINQ глубоко: LINQ глубоко: отложенное выполнение, IEnumerable и IQueryable, expression trees
- Интерфейсы коллекций: Интерфейсы коллекций: IEnumerable, ICollection, IList, IReadOnly
- JOIN и GROUP BY в SQL: JOIN: inner, left, right, full, cross, self
- Методы массивов в JS: Массивы и их методы