Введение
В современной экосистеме C# язык интегрированных запросов LINQ давно перестал быть просто удобным инструментом для работы с коллекциями — это фундаментальный парадигмальный сдвиг в мышлении разработчика. Представленный еще в 2008 году вместе с .NET Framework 3.5, LINQ кардинально изменил подход к обработке данных, унифицировав работу с массивами, коллекциями, XML, базами данных и даже удаленными источниками через единый декларативный синтаксис.
Базовый синтаксис: Method vs Query
1.1. Метод синтаксиса (Method Syntax)
var filteredUsers = users .Where(u => u.Age > 18) .OrderBy(u => u.LastName) .ThenBy(u => u.FirstName) .Select(u => new { u.FullName, u.Email }) .ToList();
1.2. Синтаксис запросов (Query Syntax)
var filteredUsers = from u in users where u.Age > 18 orderby u.LastName, u.FirstName select new { u.FullName, u.Email };
Фильтрация и проекция
2.1. Комбинированные условия Where
var activeAdults = products .Where(p => p.Price > 1000 && p.CategoryId == 5 || p.IsFeatured) .Where(p => !p.IsDeleted) .ToList();
2.2. Select с преобразованием
var productDtos = products .Select(p => new ProductDto { Id = p.Id, Name = p.Name.ToUpper(), PriceWithTax = p.Price * 1.2m, IsExpensive = p.Price > 10000 }) .ToList();
Группировка и агрегация
3.1. Группировка по нескольким полям
var salesByCategoryAndYear = orders .GroupBy(o => new { o.Category, Year = o.OrderDate.Year }) .Select(g => new { g.Key.Category, g.Key.Year, Total = g.Sum(o => o.Amount), Count = g.Count(), Average = g.Average(o => o.Amount) }) .OrderByDescending(x => x.Total) .ToList();
3.2. Агрегатные функции
var stats = products.Aggregate(new { Min = decimal.MaxValue, Max = decimal.MinValue, Total = 0m }, (acc, product) => new { Min = product.Price acc.Max ? product.Price : acc.Max, Total = acc.Total + product.Price });
Соединения (Joins)
4.1. Inner Join
var userOrders = users .Join(orders, user => user.Id, order => order.UserId, (user, order) => new { user.Name, order.Amount, order.Date }) .ToList();
4.2. GroupJoin (эквивалент LEFT JOIN)
var usersWithOrders = users .GroupJoin(orders, user => user.Id, order => order.UserId, (user, userOrders) => new { User = user, OrderCount = userOrders.Count(), TotalAmount = userOrders.Sum(o => o.Amount) }) .ToList();
Работа с коллекциями
5.1. Distinct по свойству
var uniqueCategories = products .Select(p => p.Category) .Distinct() .ToList(); // С использованием IEqualityComparer var uniqueUsers = users .DistinctBy(u => u.Email) .ToList(); // .NET 6+
5.2. Разбиение коллекций
// Пропустить первые 10, взять следующие 20 var page = users.Skip(10).Take(20).ToList(); // Разбить на группы по 100 элементов var batches = products.Chunk(100); // .NET 6+ // Разделить по условию var (expensive, cheap) = products .Partition(p => p.Price > 1000); // С помощью библиотеки MoreLinq или собственного метода
Деferred vs Immediate Execution
6.1. Отложенное выполнение (Deferred)
var query = users.Where(u => u.IsActive); // Запрос не выполняется! // Выполняется только при итерации foreach (var user in query) { /* ... */ } // Материализация создает новый запрос var activeUsers = query.ToList(); // Выполняется здесь var count = query.Count(); // Выполняется ЕЩЁ РАЗ!
6.2. Немедленное выполнение (Immediate)
// Эти методы выполняют запрос немедленно: .ToList() .ToArray() .ToDictionary() .Count() .Sum() .Average() .First() .Single()
Оптимизация производительности
7.1. Избегание множественных итераций
// ПЛОХО: Два прохода по коллекции var count = users.Where(u => u.Age > 18).Count(); var adults = users.Where(u => u.Age > 18).ToList(); // ХОРОШО: Один проход var adultList = users.Where(u => u.Age > 18).ToList(); var count = adultList.Count;
7.2. Индексы в Where
var filtered = users .Where((u, index) => u.IsActive && index % 2 == 0) // Каждый второй активный .ToList();
LINQ to Entities (Entity Framework)
8.1. Фильтрация на стороне БД
// Выполняется в SQL var users = await context.Users .Where(u => u.Age > 18 && u.Name.StartsWith("A")) .OrderBy(u => u.RegistrationDate) .Select(u => new { u.Id, u.Name }) .ToListAsync(); // Важно: ToListAsync() для асинхронности
8.2. Жадная загрузка (Eager Loading) с фильтрацией
var orders = await context.Orders .Include(o => o.Items.Where(i => i.Price > 100)) // Фильтр у связанных данных .ThenInclude(i => i.Product) .Where(o => o.Date > DateTime.UtcNow.AddDays(-30)) .ToListAsync();
Кастомные операторы и расширения
9.1. Собственный метод-расширения
public static IEnumerable ActiveUsers(this IEnumerable users) { return users.Where(u => u.IsActive && !u.IsDeleted); } // Использование var active = users.ActiveUsers().ToList();
9.2. Batch-обработка с кастомным агрегатором
public static IEnumerable > Batch (this IEnumerable source, int size) { var batch = new List (size); foreach (var item in source) { batch.Add(item); if (batch.Count == size) { yield return batch; batch = new List (size); } } if (batch.Count > 0) yield return batch; } // Использование foreach (var batch in products.Batch(100)) { await ProcessBatchAsync(batch); }
Обработка исключений и null
10.1. Безопасный доступ к свойствам
var validPrices = products .Select(p => p.Price) .Where(price => price.HasValue) .Select(price => price!.Value) // "!" после проверки .ToList();
10.2. DefaultIfEmpty для обработки отсутствующих данных
var lastOrder = user.Orders .Where(o => o.Status == OrderStatus.Completed) .OrderByDescending(o => o.Date) .FirstOrDefault() ?? Order.CreateEmpty(); // или DefaultIfEmpty()
Сложные запросы с вложенными коллекциями
var departmentReport = departments .Select(d => new { Department = d.Name, Employees = d.Employees .Where(e => e.HireDate.Year >= 2020) .GroupBy(e => e.Position) .Select(g => new { Position = g.Key, Count = g.Count(), AvgSalary = g.Average(e => e.Salary) }) .OrderByDescending(x => x.AvgSalary) .ToList() }) .Where(d => d.Employees.Any()) .OrderBy(d => d.Department) .ToList();
Заключение
LINQ в C# — это гораздо больше, чем просто удобный способ фильтрации списков. Это целостная философия работы с данными, которая воспитывает у разработчика декларативный стиль мышления и глубокое понимание операций преобразования информации.