1. Wprowadzenie
Jeśli kiedykolwiek pisałeś "klasyczne" zapytanie LINQ z grupowaniem, na przykład:
var cityCounts = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
to na pewno zauważyłeś, że do prostego liczenia ilości po grupach kod jest trochę rozwlekły. Wraz z wydaniem .NET 9 ekipa Microsoft postanowiła ułatwić życie programistom i dodała do LINQ dwie często potrzebne metody:
- CountBy — szybki sposób liczenia ilości elementów po kluczu.
- AggregateBy — uniwersalny agregator po kluczu (nie tylko liczy, ale też sumuje i tak dalej).
Przy okazji, te metody pojawiły się na podstawie wielu próśb społeczności, a ich odpowiedniki od dawna istnieją w wielu "zaawansowanych" bibliotekach LINQ, takich jak MoreLINQ.
2. Metoda CountBy: zwięzłe liczenie po grupach
Opis
CountBy — to dosłownie "grupowanie, po którym od razu idzie liczenie ilości". Sama metoda zwraca kolekcję par "klucz-ilość", co bardzo często przydaje się w realnych zadaniach analizy danych: top miasta, ilość studentów po ocenach, częstotliwość występowania czegoś.
Sygnatura (uproszczona):
IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
Zastosowanie w praktyce
Przykład 1: Ilość studentów po miastach
Załóżmy, że mamy taką klasę:
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string City { get; set; }
}
I w naszej aplikacji — lista studentów:
var students = new List<Student>
{
new Student { Name = "Iwan", Grade = 5, City = "Neonvill" },
new Student { Name = "Anna", Grade = 4, City = "Los-Santos" },
new Student { Name = "Jegor", Grade = 3, City = "Neonvill" },
new Student { Name = "Maria", Grade = 5, City = "Rosewater" },
new Student { Name = "Oleg", Grade = 4, City = "Neonvill" }
};
W .NET 8 i wcześniej trzeba by było pisać:
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} studentów");
}
W .NET 9 robi to jedna zwięzła metoda:
var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
Console.WriteLine($"{city}: {count} studentów");
}
Tak, widzisz dobrze — żadnego ręcznego GroupBy, żadnego liczenia Count() — wszystko prosto i bezpośrednio!
Przykład 2: Liczenie po ocenach
Topowe zadanie dla każdego dziekana — dowiedzieć się, ilu jest piątkowiczów i trójkowiczów w grupie:
var gradeCounts = students.CountBy(s => s.Grade);
foreach (var (grade, count) in gradeCounts)
{
Console.WriteLine($"Ocena {grade}: {count} studentów");
}
Przykład 3: Częstotliwość znaków w stringu
CountBy działa nie tylko na obiektach, ale też na prostszych rzeczach:
string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);
foreach (var (letter, count) in charFrequencies)
{
Console.WriteLine($"{letter} : {count}");
}
Jak to wygląda w pamięci (ilustracja)
| Klucz | Count (przed CountBy) | Count (z CountBy) |
|---|---|---|
|
3 | 3 |
|
1 | 1 |
|
1 | 1 |
Takie podejście sprawia, że kod jest czytelny i zmniejsza ilość błędów przy grupowaniu.
Typowe błędy i niuanse
Nie zapominaj, że CountBy zwraca krotkę (Key, Count), a nie anonimowy typ. W niektórych przypadkach wygląda to trochę mniej semantycznie, ale za to jest maksymalnie przejrzyste. Nie można też bezpośrednio wskazać, jaki typ liczby ma być dla Count — zawsze zwracany jest int.
3. Metoda AggregateBy: uniwersalne agregaty po grupach
Opis
Jeśli CountBy — to proste liczenie po kluczu, to AggregateBy — to szwajcarski scyzoryk! Chcesz policzyć sumę sprzedaży po sprzedawcy, maksymalną ocenę po klasie, albo cokolwiek, co można wyrazić akumulatorem? Wszystko to robi AggregateBy.
Sygnatura (mocno uproszczona):
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 — według czego grupować.
- seed — wartość początkowa do akumulacji.
- func — funkcja opisująca, jak "nakręcać" wynik.
Zastosowanie w praktyce
Przykład 1: Suma ocen po miastach
Zamiast takiego potwornego LINQ:
var sums = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });
Piszesz:
var sums = students.AggregateBy(
s => s.City, // grupujemy po mieście
0, // początkowa suma
(acc, s) => acc + s.Grade // dodajemy ocenę
);
foreach (var (city, sum) in sums)
{
Console.WriteLine($"{city}: suma ocen = {sum}");
}
Przykład 2: Maksymalna ocena po miastach
A chcesz maksymalną wartość — po prostu zmień akumulator:
var maxByCity = students.AggregateBy(
s => s.City,
int.MinValue, // początkowa wartość — minimalna możliwa
(max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
Console.WriteLine($"{city}: maksymalna ocena = {maxGrade}");
}
Przykład 3: Zbieramy imiona do stringa
Aktualizujemy naszą listę studentów:
var namesByCity = students.AggregateBy(
s => s.City,
"", // początkowy string
(acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);
foreach (var (city, names) in namesByCity)
{
Console.WriteLine($"{city}: {names}");
}
Ilustracja "od środka" (schemat)
. {Student, Student, Student, ...}
|
| (grupujemy po city)
v
["Neonvill"] ["Rosewater"] ["Los Santos"]
| | |
| (akumulacja) |
|___________________________|
|
v
{("Neonvill", suma), ("Rosewater", suma), ...}
Porównanie ze "starymi" sposobami
Wcześniej do wszystkich takich zadań trzeba było pisać GroupBy + Select, w środku którego był swój Sum, Aggregate albo coś jeszcze bardziej pokręconego. Teraz AggregateBy ukrywa ten "ból" i robi kod krótki i wyrazisty.
Praktyka: używamy w aplikacji
Nasz projekt edukacyjny — dziennik studentów. Dodajmy do niego raport o średniej ocenie po miastach.
// Średnią ocenę liczymy przez AggregateBy — akumulujemy sumę i ilość, potem liczymy średnią
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}: średnia ocena = {avg:F2}");
}
Tutaj akumulujemy sumę i ilość w krotce, a potem dzielimy sumę przez ilość, żeby dostać średnią.
Specyfika i pułapki
Jeśli twój akumulator to typ referencyjny, pamiętaj: w środku AggregateBy ten sam referencyjny obiekt jest używany dla wszystkich iteracji po grupie, więc nie mutuj obiektu, bo możesz przypadkiem wpłynąć na wynik całej grupy.
I jeszcze: jeśli potrzebujesz nie tylko grupowania, ale też dalszej transformacji wyników — możesz od razu użyć .Select. Staraj się wybierać semantycznie pasujące seed (wartość początkową), żeby nie dostać nieoczekiwanych wyników (np. nie używaj 0 dla stringów).
4. Porównanie starego i nowego podejścia
| Zadanie | Stary LINQ | Nowy LINQ .NET 9 |
|---|---|---|
| Ilość studentów po miastach | |
|
| Suma ocen po miastach | |
|
| Maksymalna ocena po miastach | |
|
| Częstotliwość znaków w stringu | |
|
Praktyczna korzyść w pracy i prawdziwych projektach
Takie metody jak CountBy i AggregateBy stają się niezastąpione przy pisaniu raportów statystycznych, przygotowywaniu danych do wizualizacji, budowaniu tabel przestawnych i innych typowych zadaniach. Skracają kod, czynią go bardziej czytelnym i — co najważniejsze — bardziej niezawodnym: mniej ręcznego "rozbierania na części" grupowania i agregacji, mniej szans na pomyłkę w logice.
Na rozmowie o pracę na Juniora/Middle znajomość nowych metod LINQ pokaże, że śledzisz rozwój platformy, a w produkcji — pozwoli nie wymyślać własnych rozwiązań, tylko używać eleganckich i zrozumiałych konstrukcji, które łatwo czytać i utrzymywać.
GO TO FULL VERSION