1. Вступ
Звичні методи LINQ — як меню в їдальні: обрали "суму", "мінімум" або "середнє" — і рушили далі. Та інколи хочеться чогось особливого. Тут і з’являється Aggregate — наче шеф, який готує за вашим рецептом.
Якщо порівнювати з функціональним програмуванням, Aggregate — це Reduce (або fold): ви проходите колекцією та крок за кроком згортаєте її до одного значення. Як саме — вирішуєте ви. Повний контроль і простір для творчості.
Завдання, які зручно розв’язувати за допомогою Aggregate:
- Отримання складних сум: наприклад, добуток усіх чисел; суму лише парних/непарних; суму за особливим правилом.
- Об’єднання рядків із ручною логікою (наприклад, різний роздільник для парних і непарних індексів).
- Побудова рядкових звітів («Markdown-списки», HTML, будь-який власний формат).
- Побудова колекцій зі змінюваним станом (наприклад, зібрати словник зі списку об’єктів за особливим правилом ключа).
- Будь-який складний підрахунок, який не входить до переліку стандартних агрегатних функцій.
2. Сигнатура та принцип роботи методу Aggregate
Давайте зазирнемо в офіційну документацію Microsoft щодо Enumerable.Aggregate:
public static TAccumulate Aggregate<TSource, TAccumulate>(
this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func
)
Ось що тут до чого:
- source — ваша вихідна колекція.
- seed — початкове значення (можна сприймати як стартовий акумулятор).
- func — функція, що приймає два аргументи: накопичене значення (acc), поточний елемент колекції й повертає нове накопичене значення.
Є й просте перевантаження:
public static TSource Aggregate<TSource>(
this IEnumerable<TSource> source,
Func<TSource, TSource, TSource> func
)
У цьому варіанті початковим вважається перший елемент колекції, а далі func викликається від другого до останнього.
3. Найпростіші приклади використання Aggregate
Почнімо з невеликої інтриги для студентів: чи всі знають, що Aggregate може замінити Sum або Product?
int[] numbers = { 2, 3, 4 };
// Порахувати суму
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = накопичення, val = наступний елемент
// Порахувати добуток
int product = numbers.Aggregate((acc, val) => acc * val);
Console.WriteLine(sum); // 9
Console.WriteLine(product); // 24
Виглядає як Sum і Multiply, але все робите самі! Можна жартома сказати: методом Sum завжди можна порахувати суму, а от методом Aggregate — і суму, і «анти-суму», і навіть «суму квадратних коренів».
4. Використання Aggregate для об’єднання рядків
З’єднаємо всі рядки в один, відокремлюючи їх комою (без зайвої коми в кінці):
string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// результат: "C#, LINQ, rocks"
Якщо колекція може бути порожньою, початкове значення (seed) стає у пригоді:
// Почнемо з порожнього рядка
string report = words.Aggregate(
"Технології: ",
(acc, word) => acc + word + "; ",
acc => acc.TrimEnd(' ', ';') // наприкінці приберемо зайвий ";"
);
Console.WriteLine(report); // "Технології: C#; LINQ; rocks"
Зверніть увагу на третій аргумент — функцію перетворення результату (result selector), вона є у перевантаженні з seed. Це як «останній штрих» до фінальної страви.
5. Aggregate у складі вашого застосунку
Згадаємо наш навчальний застосунок, над яким ми працюємо вже певний час. Припустімо, що маємо клас Student:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
Список студентів:
var students = new List<Student>
{
new Student { Name = "Аліса", Grade = 5 },
new Student { Name = "Боб", Grade = 4 },
new Student { Name = "Василь", Grade = 3 },
new Student { Name = "Марія", Grade = 5 }
};
Завдання: отримати рядок виду
"Найкращі: Аліса, Марія"
— тобто всіх, у кого Grade == 5.
Як зазвичай роблять початківці:
var best = "";
foreach (var s in students)
{
if (s.Grade == 5)
best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("Найкращі: " + best);
А тепер — у стилі LINQ через Aggregate:
var bestStr = students
.Where(s => s.Grade == 5)
.Select(s => s.Name)
.Aggregate("Найкращі: ", (acc, name) => acc + name + ", ")
.TrimEnd(',', ' ');
Console.WriteLine(bestStr);
Приємний бонус: мінімум коду — максимум читабельності. Навіть якщо ваш керівник не знає LINQ, він зрозуміє ваш задум (або хоча б оцінить зусилля, якщо не зрозуміє).
6. Складніші сценарії
Насправді акумулятор у Aggregate може бути не лише числом чи рядком, а й чим завгодно: хоч словником, хоч вашим власним класом або структурою.
Наприклад: зі списку студентів порахувати кількість студентів для кожної оцінки:
var gradeCounts = students.Aggregate(
new Dictionary<int, int>(),
(dict, student) => {
if (dict.ContainsKey(student.Grade))
dict[student.Grade]++;
else
dict[student.Grade] = 1;
return dict;
}
);
// Для виведення:
foreach (var pair in gradeCounts)
{
Console.WriteLine($"Оцінка {pair.Key}: {pair.Value} студентів");
}
Цей підхід фактично відтворює GroupBy вручну. Чому б просто не використати GroupBy? Іноді потрібне специфічне агрегування, якого немає серед стандартних LINQ-методів, наприклад підсумовувати лише якщо студент — не Василь, або формувати звіт із нетиповою логікою.
7. Візуалізація: як працює Aggregate (блок-схема)
Нехай маємо масив { 2, 4, 3 } і хочемо накопичити суму:
acc: 2 (перший елемент)
|
v
val: 4
acc = acc + val = 2 + 4 = 6
|
v
val: 3
acc = acc + val = 6 + 3 = 9
|
v
[Усі елементи оброблені]
|
v
Результат: 9
Аналогічна схема для перевантаження з початковим значенням (seed):
seed: 0
|
v
val: 2
acc = 0 + 2 = 2
|
v
val: 4
acc = 2 + 4 = 6
|
v
val: 3
acc = 6 + 3 = 9
|
v
Результат: 9
Порівняння Aggregate та інших агрегатних методів
| Метод | Стандартна поведінка | Гнучкість | Для порожніх колекцій | Приклад роботи |
|---|---|---|---|---|
|
Сумує числа | Низька | Повертає 0 | |
|
Підраховує елементи | Низька | Повертає 0 | |
|
Довільна агрегація | Висока | Потрібен seed | |
|
З’єднує рядки | Середня | Для порожніх — "" | |
8. Практичні поради, типові помилки та важливі моменти
Через свою гнучкість Aggregate може спричиняти неочікувані наслідки, якщо використовувати його некоректно. Найпоширеніші помилки:
Іноді програмісти забувають про seed (початкове значення) й не враховують, що якщо колекція порожня, то перевантаження без seed згенерує виняток (InvalidOperationException). Тому для порожніх колекцій використовуйте перевантаження з seed:
var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // Працює! Повертає 0
Якщо ви акумулюєте рядок через Aggregate, то легко отримати зайвий роздільник (наприклад, «,» у кінці). Краще прибрати його через .TrimEnd(',', ' ') — або використовуйте string.Join, якщо просто з’єднуєте рядки.
Акумулятор змінюваного типу (наприклад, List або Dictionary) часто використовується в Aggregate, але обережно: якщо ви змінюєте його на місці, то на кожному кроці всі посилання вказують на один об’єкт. Це може давати неочікувані ефекти під час паралельних операцій або якщо ви очікуєте копіювання. Тому в суто функціональному стилі на кожному кроці краще повертати новий об’єкт, а не змінювати старий.
У коді, який читають інші, не ускладнюйте Aggregate заради «крутості»: для початківців він виглядає складнішим за звичні foreach або стандартні агрегати. Якщо завдання тривіальне — використовуйте Sum, Count, Join. Але якщо «зібрати текст за особливим шаблоном» — Aggregate ваш найкращий друг!
9. Зв’язок із реальною роботою, співбесідами й фреймворками
В індустрії метод Aggregate часто трапляється там, де потрібні нестандартні підрахунки або згортання даних: під час формування складних звітів, статистики, графів, обчислення гешів, генерації унікальних ID або навіть для побудови UI‑компонентів з колекцій за особливою логікою.
На співбесідах питання про LINQ майже завжди трапляються такі: «Як отримати суму елементів масиву?», «Як перетворити список рядків в один рядок?» або «Як реалізувати підрахунок кількості унікальних елементів?» — і часто очікують розв’язань за допомогою LINQ, зокрема Aggregate. Питання можуть бути й креативними: «Чи можете за допомогою LINQ порахувати суму квадратів парних чисел, а потім повернути рядок, що описує цей процес?»
У багатьох популярних .NET‑бібліотеках і фреймворках на кшталт Entity Framework, Dapper, RavenDB метод Aggregate рідко застосовують безпосередньо на стороні бази даних, але на рівні коду його активно використовують для агрегації в пам’яті.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ