1. Wprowadzenie
Stare-dobre metody LINQ — jak jedzenie na stołówce: wybierasz "sumę", "minimum" albo "średnią" i lecisz dalej. Ale czasem chce się czegoś ekstra. I tu wchodzi Aggregate — jak szef kuchni, który gotuje według twojego przepisu.
Jak porównać z programowaniem funkcyjnym, Aggregate to taki Reduce (albo fold): przechodzisz przez kolekcję i krok po kroku zwijasz ją do jednej wartości. Jak dokładnie — decydujesz sam. Pełna kontrola, pełna kreatywność.
Zadania, które fajnie ogarnąć z Aggregate:
- Wyciąganie skomplikowanych sum: np. iloczyn wszystkich liczb, albo suma tylko parzystych/nieparzystych, albo suma według własnej zasady.
- Łączenie stringów z własną logiką (np. inny separator dla parzystych i nieparzystych indeksów).
- Budowanie raportów tekstowych ("Markdown-listy", HTML, dowolny customowy format).
- Tworzenie kolekcji ze zmiennym stanem (np. budowanie słownika z listy obiektów według własnej zasady klucza).
- Każde skomplikowane liczenie, które nie wchodzi w standardowe funkcje agregujące.
2. Sygnatura i zasada działania metody Aggregate
Zerknijmy do oficjalnej dokumentacji Microsoft do Enumerable.Aggregate:
public static TAccumulate Aggregate<TSource, TAccumulate>(
this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func
)
O co tu chodzi:
- source — twoja wejściowa kolekcja.
- seed — "wartość początkowa" (można traktować jako startowy akumulator).
- func — funkcja przyjmująca dwa argumenty: zgromadzoną wartość (acc), aktualny element kolekcji i zwracająca nową zgromadzoną wartość.
Jest też prostsza wersja:
public static TSource Aggregate<TSource>(
this IEnumerable<TSource> source,
Func<TSource, TSource, TSource> func
)
W tej wersji "początkowym" jest pierwszy element kolekcji, a dalej func leci od drugiego do ostatniego.
3. Najprostsze przykłady użycia Aggregate
Zaczniemy od mini-zagadki dla studentów: czy wszyscy wiedzą, że Aggregate może zastąpić Sum albo Product?
int[] numbers = { 2, 3, 4 };
// Policz sumę
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = zgromadzenie, val = kolejny element
// Policz iloczyn
int product = numbers.Aggregate((acc, val) => acc * val);
Console.WriteLine(sum); // 9
Console.WriteLine(product); // 24
Wygląda jak Sum i Multiply, ale robimy to sami! Można zażartować: metodą Sum zawsze policzysz sumę, a metodą Aggregate — i sumę, i "anty-sumę", i nawet "sumę pierwiastków kwadratowych".
4. Użycie Aggregate do łączenia stringów
Sklejamy wszystkie stringi w jeden, oddzielając przecinkiem (bez zbędnego przecinka na końcu):
string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// wynik: "C#, LINQ, rocks"
Jeśli kolekcja może być pusta, wartość początkowa (seed) jest mega wygodna:
// Zaczynamy od pustego stringa
string report = words.Aggregate(
"Technologie: ",
(acc, word) => acc + word + "; ",
acc => acc.TrimEnd(' ', ';') // na końcu usuwamy zbędny ";"
);
Console.WriteLine(report); // "Technologie: C#; LINQ; rocks"
Zwróć uwagę na trzeci argument — funkcję przetwarzającą wynik (result selector), jest w wersji z seed. To jak "deser" na koniec dania.
5. Aggregate w twojej aplikacji
Przypomnijmy sobie naszą aplikację edukacyjną, z którą już trochę się bawimy. Załóżmy, że mamy klasę Student:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
Lista studentów:
var students = new List<Student>
{
new Student { Name = "Alicja", Grade = 5 },
new Student { Name = "Bob", Grade = 4 },
new Student { Name = "Wiktor", Grade = 3 },
new Student { Name = "Maria", Grade = 5 }
};
Zadanie: uzyskać string w stylu
"Najlepsi: Alicja, Maria"
— czyli wszystkich, którzy mają Grade == 5.
Jak robią to początkujący:
var best = "";
foreach (var s in students)
{
if (s.Grade == 5)
best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("Najlepsi: " + best);
A teraz — LINQ-style przez Aggregate:
var bestStr = students
.Where(s => s.Grade == 5)
.Select(s => s.Name)
.Aggregate("Najlepsi: ", (acc, name) => acc + name + ", ")
.TrimEnd(',', ' ');
Console.WriteLine(bestStr);
Piękno: minimum kodu, maksimum czytelności. Nawet jeśli twój szef nie zna LINQ, zrozumie twój zamysł (albo przynajmniej doceni starania, jeśli nie zrozumie).
6. Bardziej zaawansowane scenariusze
Tak naprawdę, akumulator w Aggregate może być nie tylko liczbą czy stringiem, ale czymkolwiek: nawet słownikiem, własną klasą czy strukturą.
Na przykład: z listy studentów policzyć ilu studentów ma każdą ocenę:
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;
}
);
// Do wyświetlenia:
foreach (var pair in gradeCounts)
{
Console.WriteLine($"Ocena {pair.Key}: {pair.Value} studentów");
}
To podejście w sumie robi GroupBy, ale ręcznie. Dlaczego nie użyć po prostu GroupBy? Czasem trzeba specjalnej agregacji, której nie ma w standardowych metodach LINQ, np. sumować tylko jeśli student to nie Wiktor, albo budować raport pod raport.
7. Wizualizacja: jak działa Aggregate (schemat blokowy)
Załóżmy, że mamy tablicę { 2, 4, 3 } i chcemy zsumować:
acc: 2 (pierwszy element)
|
v
val: 4
acc = acc + val = 2 + 4 = 6
|
v
val: 3
acc = acc + val = 6 + 3 = 9
|
v
[Wszystkie elementy przetworzone]
|
v
Wynik: 9
Podobny schemat dla wersji z 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
Wynik: 9
Porównanie Aggregate i innych metod agregujących
| Metoda | Standardowe zachowanie | Elastyczność | Dla pustych kolekcji | Przykład działania |
|---|---|---|---|---|
|
Sumuje liczby | Niska | Zwraca 0 | |
|
Liczy elementy | Niska | Zwraca 0 | |
|
Dowolne liczenie | Wysoka | Wymaga seed | |
|
Łączy stringi | Średnia | Dla pustych = "" | |
8. Praktyczne tipy, typowe wtopy i ważne momenty
Przez swoją elastyczność, Aggregate może prowadzić do niespodzianek, jeśli użyjesz go źle. Najczęstsze wtopy:
Czasem programiści zapominają o seed (wartości początkowej) i nie biorą pod uwagę, że jeśli kolekcja jest pusta, to wersja bez seed rzuci wyjątek (InvalidOperationException). Dlatego dla pustych kolekcji używaj wersji z seed:
var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // Działa! Zwróci 0
Jeśli akumulujesz string przez Aggregate, łatwo dostać zbędny separator (np. "," na końcu). Lepiej usunąć go przez .TrimEnd(',', ' ') — albo użyj string.Join, jeśli po prostu sklejasz stringi.
Akumulator typu zmiennego (np. List albo Dictionary) często jest używany w Aggregate, ale ostrożnie: jeśli go zmieniasz w miejscu (in-place), to na każdym kroku wszystkie referencje wskazują na ten sam obiekt. To może dać dziwne efekty przy operacjach równoległych albo jeśli oczekujesz kopiowania. Dlatego w czystym stylu funkcyjnym na każdym kroku lepiej zwracać nowy obiekt, a nie zmieniać stary.
W kodzie, który czytają inni, nie komplikuj Aggregate na siłę dla "fajności": dla początkujących jest trudniejszy niż zwykłe foreach albo standardowe agregaty. Jeśli zadanie jest banalne — użyj Sum, Count, Join. Ale jeśli "złożyć tekst według własnego szablonu" — Aggregate to twój ziomek!
9. Związek z realną robotą, rozmowami kwalifikacyjnymi i frameworkami
W branży metoda Aggregate pojawia się często tam, gdzie trzeba nietypowych liczeń albo zwijania danych, np. przy budowaniu skomplikowanych raportów, statystyk, grafów, liczeniu hashy, generowaniu unikalnych id albo nawet do budowania komponentów UI z kolekcji według własnej logiki.
Na rozmowach kwalifikacyjnych pytania o LINQ prawie zawsze się pojawiają: "Jak policzyć sumę elementów tablicy?", "Jak zamienić listę stringów w jeden string?" albo "Jak policzyć ilość unikalnych elementów?" — i często oczekują rozwiązań z LINQ, w tym Aggregate. Pytania mogą być też kreatywne: "Czy przez LINQ policzysz sumę kwadratów parzystych liczb, a potem zwrócisz string opisujący ten proces?"
W wielu popularnych bibliotekach i frameworkach .NET, jak Entity Framework, Dapper, RavenDB, metoda Aggregate rzadko jest używana bezpośrednio po stronie bazy, ale na poziomie kodu jest bardzo przydatna do agregacji w pamięci.
GO TO FULL VERSION