CodeGym /Kursy /C# SELF /Nowe metody LINQ: CountBy<...

Nowe metody LINQ: CountBy i AggregateBy

C# SELF
Poziom 32 , Lekcja 2
Dostępny

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)
"Neonvill"
3 3
"Rosewater"
1 1
"Los Santos"
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
GroupBy + Select + Count
students.CountBy(s => s.City)
Suma ocen po miastach
GroupBy + Select + Sum
students.AggregateBy(..., 0, ...)
Maksymalna ocena po miastach
GroupBy + Select + Max
students.AggregateBy(..., ...)
Częstotliwość znaków w stringu
GroupBy + Select + Count
word.CountBy(ch => ch)

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ć.

2
Zadanie
C# SELF, poziom 32, lekcja 2
Niedostępne
Określenie maksymalnej oceny według miast
Określenie maksymalnej oceny według miast
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION