1. Wprowadzenie
Pracując z kolekcjami, często nie chodzi tylko o przejście po każdym elemencie i zrobienie czegoś, ale o uzyskanie krótkiego "podsumowania" całego zbioru danych. Na przykład:
- Policzyć, ilu studentów dostało piątkę.
- Dowiedzieć się, ilu jest wszystkich uczniów w szkole.
- Policzyć, jaką sumę punktów zdobyli wszyscy studenci na egzaminie.
- Znaleźć maksymalną i minimalną ocenę.
- Obliczyć średnią ocenę w klasie.
Oczywiście, wszystkie te zadania da się rozwiązać zwykłą pętlą z licznikami. Ale bądźmy szczerzy: mało kto się cieszy na myśl o pisaniu dwudziestu linijek kodu do tak prostej rzeczy.
LINQ daje zestaw funkcji agregujących — gotowych metod, które biorą kolekcję, przelatują po wszystkich elementach i zwracają odpowiedź: sumę, średnią, maksimum, minimum, a czasem coś bardziej sprytnego.
Krótka "tabela agregatów":
| Metoda | Co robi | Zwraca |
|---|---|---|
|
Liczy ilość elementów | |
|
Liczy sumę wartości liczbowych | Zależy od typu elementów (, , ...) |
|
Oblicza średnią arytmetyczną | lub inny typ liczbowy |
|
Szuka maksymalny element | Element kolekcji |
|
Szuka minimalny element | Element kolekcji |
Wszystkie te metody to extension-metody dla kolekcji implementujących IEnumerable<T>. Więcej — oficjalna dokumentacja LINQ metod agregujących.
2. Przygotowujemy dane do ćwiczeń
Lecimy dalej z prostą apką do ewidencji studentów. Załóżmy, że mamy taką klasę:
// Model studenta
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // Ocena w pięciostopniowej skali
public string Email { get; set; }
}
I listę:
// Podstawowa lista studentów
List<Student> students = new List<Student>
{
new Student { Name = "Ivan", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "Olga", Grade = 4, Email = "olga@example.com" },
new Student { Name = "Artem", Grade = 3, Email = "artem@example.com" },
new Student { Name = "Darya", Grade = 5, Email = "darya@example.com" },
new Student { Name = "Petr", Grade = 2, Email = "petr@example.com" }
};
3. Liczenie elementów: Count()
Najprostsza statystyka
Załóżmy, że chcesz wiedzieć, ilu masz studentów:
int totalStudents = students.Count(); // 5
Console.WriteLine($"Wszystkich studentów: {totalStudents}");
LINQ-magija: Proste! Metoda Count() zwraca ilość elementów w kolekcji.
Liczenie z warunkiem
A ilu jest prymusów w klasie?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"Piątkowiczów: {excellentStudents}");
Tutaj przekazujemy do Count lambdę — zwróci tylko tych, którzy spełniają warunek (s.Grade == 5). W środku LINQ to to samo co Where(...).Count(), ale krócej i trochę szybciej.
A jeśli kolekcja jest pusta?
Jeśli kolekcja jest pusta, Count zwróci po prostu 0 — żadnego błędu nie będzie.
4. Sumowanie wartości: Sum()
Sumujemy wszystkie oceny
Załóżmy, że chcesz znać sumę punktów zdobytych przez klasę:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"Suma wszystkich ocen: {sumOfGrades}");
Sum przyjmuje selektor (wyrażenie lambda), które zwraca wartość dla każdego elementu.
Sumowanie po kolekcji liczb
Jeśli masz po prostu listę liczb, selektor niepotrzebny:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
Typowe błędy i niuanse
Jeśli kolekcja jest pusta, Sum() dla typów liczbowych zwróci 0. Ale jeśli kolekcja to nullable-typy (int?, double?), to Sum też działa poprawnie: ignoruje null wartości.
5. Średnia arytmetyczna: Average()
Liczymy średnią ocenę w klasie
Klasyka: ile średnio dostał student?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"Średnia ocena: {averageGrade:F2}");
Average — bardzo przydatny kumpel do statystyki. Uwaga: zwracana wartość to zawsze double, nawet jeśli wejściowe były int. Dzięki temu nie tracisz części ułamkowej.
Jeśli nie ma żadnego elementu
Jeśli kolekcja jest pusta, wywołanie Average() rzuci wyjątek InvalidOperationException. To bardzo częsta pułapka: jeśli nie masz pewności, że coś jest w kolekcji, sprawdź to wcześniej!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("Brak danych do liczenia średniej!");
Średnia po tablicy liczb
double avg = numbers.Average();
6. Maksimum i minimum: Max() i Min()
Kto w klasie jest gwiazdą, a kto na dnie
Chcesz wiedzieć, kto ciągnie klasę na ocenach, a kto... no, daje powody do niepokoju nauczycielowi? Proste:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"Maksymalna ocena: {maxGrade}");
Console.WriteLine($"Minimalna ocena: {minGrade}");
A kto dokładnie jest tym bohaterem?
Czasem ważna nie tylko cyfra, ale imię tego, kto ją zdobył. Bierzemy obiekt studenta z najwyższą oceną:
// Bierzemy pierwszego prymusa po sortowaniu malejąco
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"Najlepszy student: {bestStudent.Name} ({bestStudent.Grade})");
W nowszych wersjach .NET jest też MaxBy, który pozwala zrobić to ładniej. Od .NET 6 już dostępny, a w .NET 9 jeszcze wygodniej (patrz MaxBy na Microsoft Docs). Ale nawet bez MaxBy można spokojnie ogarnąć sortowaniem i .First().
Pułapki i różnice
Jeśli kolekcja jest pusta, wywołanie Max() i Min() też rzuci InvalidOperationException. Warto być na to gotowym (szczególnie jeśli nie sprawdzałeś kolekcji wcześniej):
if (students.Any())
Console.WriteLine($"Maksymalna ocena: {students.Max(s => s.Grade)}");
else
Console.WriteLine("Brak studentów do szukania maksimum.");
7. Przykłady "łańcuchów" metod agregujących
Często metody agregujące używa się razem z filtrowaniem i projekcją:
// Średnia ocena wśród piątkowiczów
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade); // zawsze 5, ale przykład jest czytelny
// Suma punktów wśród studentów z oceną co najmniej 4
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// Ilość unikalnych maili (na wszelki wypadek)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
Tu LINQ zaczyna "grać" na całego: proste łączenie operacji pozwala pisać wyrazisty i czytelny kod, który zrozumie nawet twój kot (no, jeśli kot to Junior C# Developer).
8. Porównanie z "ręcznym" podejściem: po co używać agregatów?
Dla jasności porównajmy LINQ ze zwykłymi pętlami na przykładzie liczenia średniej:
Zwykłe podejście:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
Nawet obsługa pustej kolekcji — prościej!
9. Przydatne niuanse
Krótko o wydajności i szczegółach implementacji
LINQ-agregaty, w przeciwieństwie do większości operacji LINQ, wykonują się od razu! Czyli gdy wywołujesz Sum(), Count(), Average(), Max(), Min(), cała iteracja po kolekcji dzieje się w tym momencie. Te metody zwracają nie kolekcje, a jeden końcowy wynik.
To ważne: jeśli zrobiłeś coś ciężkiego przed agregatem, np. złożone filtrowanie czy przekształcenie — wykona się to tylko raz, w momencie wywołania agregatu.
Czy metody agregujące wspierają query-syntax?
Przed startem z LINQ często ktoś pyta: "A można to pisać w query-syntax?" Krótko: sam query-syntax nie ma słów kluczowych dla agregatów, ale zawsze możesz mieszać z method syntax:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
W środku nawiasów — zwykły query-syntax, a potem wywołujesz metodę agregującą. Tak się robi częściej, niż myślisz!
10. Typowe błędy przy użyciu LINQ
Błąd nr 1: próba użycia Average(), Max() lub Min() na pustej kolekcji.
Jeśli kolekcja jest pusta, Sum() i Count() spokojnie zwrócą 0, ale Average(), Max() i Min() rzucą wyjątek. Przed wywołaniem tych metod upewnij się, że kolekcja ma choć jeden element.
Błąd nr 2: przekazanie lambdy z błędną sygnaturą.
Na przykład, jeśli przekażesz string zamiast liczby do funkcji agregującej (Sum, Max itd.), dostaniesz błąd kompilacji. Łatwo się pomylić przy użyciu anonimowych metod.
Błąd nr 3: nieoptymalne sprawdzanie ilości elementów.
Metoda Where(...).Count() najpierw tworzy nową kolekcję, potem liczy elementy. Zamiast tego użyj Count(predicate) — od razu liczy pasujące elementy i działa szybciej.
Błąd nr 4: ignorowanie specyfiki typów nullable przy agregacji.
Jeśli liczysz sumę po int?, Sum() zignoruje null-wartości. To poprawne zachowanie, ale czasem może dać nieoczekiwany wynik, jeśli nie bierzesz tego pod uwagę.
GO TO FULL VERSION