Найти в Дзене
Prizrak Developer

LINQ в C#: от базовых запросов до производительности

В современной экосистеме 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 => !
Оглавление

Введение

В современной экосистеме 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# — это гораздо больше, чем просто удобный способ фильтрации списков. Это целостная философия работы с данными, которая воспитывает у разработчика декларативный стиль мышления и глубокое понимание операций преобразования информации.