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 => !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.Min ? product.Price : acc.Min,
    Max = 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<User> ActiveUsers(this IEnumerable<User> users)
{
    return users.Where(u => u.IsActive && !u.IsDeleted);
}

// Использование
var active = users.ActiveUsers().ToList();

9.2. Batch-обработка с кастомным агрегатором

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    var batch = new List<T>(size);
    foreach (var item in source)
    {
        batch.Add(item);
        if (batch.Count == size)
        {
            yield return batch;
            batch = new List<T>(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# — это гораздо больше, чем просто удобный способ фильтрации списков. Это целостная философия работы с данными, которая воспитывает у разработчика декларативный стиль мышления и глубокое понимание операций преобразования информации.