JavaRush /Курсы /C# SELF /Новые методы LINQ: CountBy...

Новые методы LINQ: CountBy и AggregateBy

C# SELF
32 уровень , 2 лекция
Открыта

1. Введение

Если вы когда-нибудь писали "классический" LINQ-запрос с группировкой, например:


var cityCounts = students.GroupBy(s => s.City)
                         .Select(g => new { City = g.Key, Count = g.Count() });

то наверняка замечали, что для простого подсчёта количества по группам код выглядит несколько многословно. С выходом .NET 9 команда Microsoft решила облегчить жизнь разработчикам и добавила в LINQ два часто востребованных метода:

  • CountBy — быстрый способ подсчёта количества элементов по ключу.
  • AggregateBy — универсальный агрегатор по ключу (не только считает, но и суммирует, и так далее).

Кстати, эти методы появились по мотивам большого количества запросов от сообщества, а их аналоги давно существуют во многих "продвинутых" LINQ-библиотеках, таких, как MoreLINQ.

2. Метод CountBy: лаконичный подсчёт по группам

Описание

CountBy — это буквально "группировка, после которой сразу идёт подсчёт количества". Сам по себе метод возвращает коллекцию пар "ключ-количество", что очень часто нужно в реальных задачах анализа данных: топ-города, количество студентов по оценкам, частота встречаемости чего-либо.

Сигнатура (упрощённо):


IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)

Применение на практике

Пример 1: Количество студентов по городам

Допустим, у нас есть такой класс:


class Student
{
    public string Name { get; set; }
    public int Grade { get; set; }
    public string City { get; set; }
}

И в нашем приложении — список студентов:


var students = new List<Student>
{
    new Student { Name = "Иван", Grade = 5, City = "Неонвилль" },
    new Student { Name = "Анна", Grade = 4, City = "Лос-Сантос" },
    new Student { Name = "Егор", Grade = 3, City = "Неонвилль" },
    new Student { Name = "Мария", Grade = 5, City = "Роузвотер" },
    new Student { Name = "Олег", Grade = 4, City = "Неонвилль" }
};

В .NET 8 и ранее пришлось бы писать:


var group = students.GroupBy(s => s.City)
                    .Select(g => new { City = g.Key, Count = g.Count() });

foreach (var cityInfo in group)
{
    Console.WriteLine($"{cityInfo.City}: {cityInfo.Count} студентов");
}

В .NET 9 этим занимается один лаконичный метод:


var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
    Console.WriteLine($"{city}: {count} студентов");
}

Да, вы видите правильно — никакой ручной GroupBy, никакого расчёта Count() — всё просто и прямо!

Пример 2: Подсчёт по оценкам

Топовая задача для любого декана — узнать, сколько отличников и троечников в группе:


var gradeCounts = students.CountBy(s => s.Grade);

foreach (var (grade, count) in gradeCounts)
{
    Console.WriteLine($"Оценка {grade}: {count} студентов");
}

Пример 3: Частота символов в строке

CountBy применим не только к объектам, но и к более простым штукам:


string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);

foreach (var (letter, count) in charFrequencies)
{
    Console.WriteLine($"{letter} : {count}");
}

Как это выглядит в памяти (иллюстрация)

Ключ Count (до CountBy) Count (с CountBy)
"Неонвилль"
3 3
"Роузвотер"
1 1
"Лос Сантос"
1 1

Такой подход делает код читаемым и сокращает количество ошибок при группировках.

Типичные ошибки и нюансы

Не забывайте, что CountBy возвращает кортеж (Key, Count), а не анонимный тип. В некоторых случаях это выглядит чуть менее семантично, но зато максимально прозрачно. Также нельзя напрямую указать, какой тип числа должен быть для Count — всегда возвращается int.

3. Метод AggregateBy: универсальные агрегаты по группам

Описание

Если CountBy — это простой подсчёт по ключу, то AggregateBy — швейцарский нож! Хотите посчитать сумму продаж по продавцу, максимальную оценку по классу, или вообще что угодно, что можно выразить аккумулятором? Всё это делает AggregateBy.

Сигнатура (сильно упрощено):


IEnumerable<(TKey Key, TAccumulate Result)> AggregateBy<TSource, TKey, TAccumulate>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func)
  • keySelector — по какому признаку группировать.
  • seed — начальное значение для аккумуляции.
  • func — функция, описывающая, как "накручивать" итог.

Применение на практике

Пример 1: Сумма оценок по городам

Вместо такого монструозного LINQ:


var sums = students.GroupBy(s => s.City)
                   .Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });

Вы пишете:


var sums = students.AggregateBy(
    s => s.City, // группируем по городу
    0,           // начальная сумма
    (acc, s) => acc + s.Grade // прибавляем оценку
);

foreach (var (city, sum) in sums)
{
    Console.WriteLine($"{city}: сумма оценок = {sum}");
}

Пример 2: Максимальная оценка по городам

А хотите максимальное значение — просто меняйте аккумулятор:


var maxByCity = students.AggregateBy(
    s => s.City,
    int.MinValue, // начальное значение — минимально возможное
    (max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
    Console.WriteLine($"{city}: максимальная оценка = {maxGrade}");
}

Пример 3: Собираем имена в строку

Обновим наш список студентов:


var namesByCity = students.AggregateBy(
    s => s.City,
    "", // начальная строка
    (acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);

foreach (var (city, names) in namesByCity)
{
    Console.WriteLine($"{city}: {names}");
}

Иллюстрация "изнутри" (схема)


.   {Student, Student, Student, ...}
               |
               | (группируем по city)
               v
["Неонвилль"]  ["Роузвотер"]  ["Лос Сантос"]
      |           |               |
      |     (аккумуляция)         |
      |___________________________|
            |
            v
{("Неонвилль", сумма), ("Роузвотер", сумма), ...}

Сравнение со "старыми" способами

Раньше для всех подобных задач приходилось писать GroupBy + Select, внутри которого был свой Sum, Aggregate или ещё что-то хитрое. Теперь AggregateBy скрывает эту "боль" и делает код коротким и выразительным.

Практика: применяем в приложении

Наш учебный проект — дневник студентов. Добавим в него отчёт по среднему баллу по городам.


// Средний балл вычисляем с помощью AggregateBy — аккумулируем сумму и количество, потом считаем среднее
var avgByCity = students.AggregateBy(
    s => s.City,
    (Sum: 0, Count: 0),
    (acc, s) => (acc.Sum + s.Grade, acc.Count + 1)
).Select(x => (x.Key, Average: (double)x.Result.Sum / x.Result.Count));

foreach (var (city, avg) in avgByCity)
{
    Console.WriteLine($"{city}: средний балл = {avg:F2}");
}

Здесь мы аккумулируем сумму и количество в кортеже, а потом делим сумму на количество для получения среднего.

Особенности и подводные камни

Если ваш аккумулятор — ссылочный тип, не забывайте: внутри AggregateBy одна и та же ссылка используется для всех итераций по группе, так что не мутируйте объект, иначе можно случайно повлиять на результат всей группы.

И ещё: если нужна не только группировка, но и дальнейшая трансформация результатов — можно сразу применять .Select. Старайтесь выбирать семантически подходящий seed (начальное значение), чтобы не получить неожиданные результаты (например, не используйте 0 для строк).

4. Сравнение старого и нового подхода

Задача Старый LINQ Новый LINQ .NET 9
Количество студентов по городам
GroupBy + Select + Count
students.CountBy(s => s.City)
Сумма оценок по городам
GroupBy + Select + Sum
students.AggregateBy(..., 0, ...)
Максимальная оценка по городам
GroupBy + Select + Max
students.AggregateBy(..., ...)
Частота символов в строке
GroupBy + Select + Count
word.CountBy(ch => ch)

Практическая польза для работы и реальных проектов

Такие методы как CountBy и AggregateBy становятся незаменимыми при написании статистических отчётов, подготовке данных для визуализации, построении сводных таблиц и прочих типовых задач. Они сокращают код, делают его более читаемым и, главное, более надёжным: меньше ручного "разбора полётов" с группировкой и агрегацией, меньше шансов ошибиться с логикой.

На собеседовании на позицию Junior/Middle знание новых LINQ-методов покажет, что вы следите за развитием платформы, а в продакшене — позволит не городить свои велосипеды, а использовать изящные и понятные конструкции, которые легко читать и поддерживать.

2
Задача
C# SELF, 32 уровень, 2 лекция
Недоступна
Определение максимальной оценки по городам
Определение максимальной оценки по городам
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ