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 |
|---|---|---|
| Кількість студентів за містами | |
|
| Сума оцінок за містами | |
|
| Максимальна оцінка за містами | |
|
| Частота символів у рядку | |
|
Практична користь для роботи й реальних проєктів
Такі методи, як CountBy та AggregateBy, стають незамінними під час написання статистичних звітів, підготовки даних для візуалізації, побудови зведених таблиць та інших типових завдань. Вони скорочують код, роблять його читабельнішим і, головне, надійнішим: менше ручної мороки з групуванням та агрегацією, менше шансів помилитися в логіці.
На співбесіді на позицію Junior/Middle знання нових LINQ‑методів покаже, що ви стежите за розвитком платформи, а в продакшні — дасть змогу не вигадувати велосипед, а використовувати витончені й зрозумілі конструкції, які легко читати й підтримувати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ