1. Wprowadzenie
Grupowanie danych — to podział elementów kolekcji na "koszyki" w zależności od wspólnej cechy. Wyobraź sobie, że sortujesz jabłka według koloru: wszystkie czerwone wrzucasz do jednego koszyka, zielone — do drugiego, żółte — jeszcze gdzieś indziej. W programowaniu to się nazywa grupowaniem.
Po co to komu?
W realnych zadaniach często przydaje się pogrupować ludzi według miasta, produkty według kategorii, transakcje według daty i tak dalej. To pozwala robić fajną analizę: na przykład dowiedzieć się, ilu studentów jest w każdej klasie albo które miasta są najbardziej studenckie.
LINQ daje do tego specjalną metodę: GroupBy. Jest też wygodny, podobny operator w query syntax — to group by. Zobaczmy, jak to działa na przykładach z naszej aplikacji do ogarniania studentów i klas.
2. GroupBy w method syntax
Podstawowa sygnatura i co zwraca
Metoda GroupBy wygląda trochę strasznie:
IEnumerable<IGrouping<TKey, TElement>> GroupBy<TElement, TKey>(
this IEnumerable<TElement> source,
Func<TElement, TKey> keySelector
)
Gdzie:
- source — oryginalna kolekcja (np. lista studentów).
- keySelector — funkcja, według której określamy cechę grupowania, np. właściwość ClassName albo City.
Ważne!
Wynik — to nie po prostu tablica twoich obiektów, tylko kolekcja specjalnych grup (IGrouping<TKey, TElement>). Każda grupa zawiera:
- Klucz grupy (Key — to, co połączyło elementy),
- Kolekcję elementów, które wpadły do tej grupy.
Schemat:
| Klucz (Key) | Elementy grupy (grupa obiektów) |
|---|---|
| "9A" | Jan, Olek, Maria |
| "10B" | Svetlana, Piotr |
| ... | ... |
Przykład 1: Grupujemy studentów według nazwy klasy
Załóżmy, że mamy klasę Student:
public class Student
{
public string Name { get; set; }
public string ClassName { get; set; }
public int Grade { get; set; }
}
I kolekcję studentów:
var students = new List<Student>
{
new Student { Name = "Jan", ClassName = "9A", Grade = 5 },
new Student { Name = "Olek", ClassName = "9A", Grade = 4 },
new Student { Name = "Maria", ClassName = "10B", Grade = 5 },
new Student { Name = "Svetlana", ClassName = "10B", Grade = 3 }
};
Grupujemy studentów według klasy:
var studentsByClass = students.GroupBy(s => s.ClassName);
foreach (var group in studentsByClass)
{
Console.WriteLine($"Klasa: {group.Key}"); // Klucz grupy
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}, ocena: {student.Grade}");
}
}
Co się dzieje?
Metoda GroupBy zrobiła nam dwie grupy: jedną dla "9A", drugą — dla "10B". Przechodząc po grupach, możemy wyświetlać dowolne zagregowane info o każdej "kupce".
Wizualizacja: jak wygląda kolekcja po GroupBy
Można to sobie wyobrazić tak:
students.GroupBy(s => s.ClassName)
├─ grupa "9A" { Jan, Olek }
└─ grupa "10B" { Maria, Svetlana }
Każda "gałąź" — osobna mini-lista studentów, połączonych według klucza.
Studenci według oceny: grupowanie po dowolnych cechach
Spróbujmy pogrupować nie według klas, tylko według ocen!
var studentsByGrade = students.GroupBy(s => s.Grade);
foreach (var group in studentsByGrade)
{
Console.WriteLine($"Ocena: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name} z klasy {student.ClassName}");
}
}
Wynik:
Ocena: 5
- Jan z klasy 9A
- Maria z klasy 10B
Ocena: 4
- Olek z klasy 9A
Ocena: 3
- Svetlana z klasy 10B
3. Przetwarzanie grup w LINQ: co można robić po GroupBy
Bardzo często po grupowaniu chcemy nie tylko wyświetlić grupy, ale dostać jakąś zagregowaną informację — np. policzyć ilość elementów w każdej grupie, dostać średnią ocenę, zebrać listę imion.
Przykład 2: Liczenie liczby studentów w każdej klasie
var classCounts = students
.GroupBy(s => s.ClassName)
.Select(g => new { Class = g.Key, Count = g.Count() });
foreach (var cc in classCounts)
{
Console.WriteLine($"W klasie {cc.Class} — {cc.Count} studentów");
}
Wynik:
W klasie 9A — 2 studentów
W klasie 10B — 2 studentów
- Najpierw podzieliliśmy studentów na grupy.
- Potem dla każdej grupy stworzyliśmy anonimowy obiekt z kluczem (nazwa klasy) i ilością (metoda Count()).
Przykład 3: Zebrać listę imion prymusów według klasy
var excellentByClass = students
.Where(s => s.Grade == 5)
.GroupBy(s => s.ClassName)
.Select(g => new
{
Class = g.Key,
ExcellentStudents = g.Select(s => s.Name).ToList()
});
foreach (var group in excellentByClass)
{
Console.WriteLine($"W klasie {group.Class} prymusi: {string.Join(", ", group.ExcellentStudents)}");
}
4. group by w query syntax
Jeśli masz ochotę na coś podobnego do SQL albo przyszedłeś ze świata baz danych, to LINQ wspiera składnię zapytań typu group by ... into ....
Podstawowy przykład grupowania
var studentsByClass =
from s in students
group s by s.ClassName into classGroup
select classGroup;
foreach (var group in studentsByClass)
{
Console.WriteLine($"Klasa: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
Najpierw "grupujemy" według cechy, potem nadajemy grupie nazwę (into classGroup) — i dalej możemy z nią robić, co chcemy.
Projekcja z agregatami
Załóżmy, że chcemy poznać miasta i liczbę studentów z każdego miasta:
var countsByCity =
from s in students
group s by s.ClassName into classGroup
select new { Class = classGroup.Key, Count = classGroup.Count() };
foreach (var item in countsByCity)
{
Console.WriteLine($"W klasie {item.Class} — {item.Count} studentów");
}
— Składnia ta sama, ale po select można robić dowolną projekcję (agregację, listy, średnie wartości itd.).
5. Dodatkowe scenariusze
Grupowanie po kilku cechach (klucz złożony)
Czasem trzeba grupować po kombinacji właściwości. Na przykład po klasie i ocenie:
var byClassAndGrade = students
.GroupBy(s => new { s.ClassName, s.Grade });
foreach (var group in byClassAndGrade)
{
Console.WriteLine($"Klasa: {group.Key.ClassName}, Ocena: {group.Key.Grade}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
Klucz grupy teraz — to anonimowy typ (kombinacja kilku pól).
Jak działają grupy w praktyce?
Trochę kuchni LINQ
- Kiedy robisz GroupBy, LINQ buduje wewnątrz specjalną "tabelę": dla każdej unikalnej wartości klucza — osobna grupa.
- Grupy implementują interfejs IGrouping<TKey, TElement>. Możesz korzystać z właściwości Key, żeby dostać wartość cechy, według której grupowałeś.
Lazy evaluation i wydajność
- Grupy są liczone dopiero wtedy, gdy zaczynasz je przeglądać (foreach). Więc jeśli masz bardzo duże kolekcje — nie bój się "tworzenia" grup, póki do nich nie zajrzysz, one żyją tylko w momencie użycia.
- Jeśli planujesz wielokrotnie korzystać z grup — możesz wywołać .ToList() albo .ToArray(), żeby "utrwalić" wynik.
Praktyczne zastosowanie
Grupowanie pozwala budować złożone raporty, statystyki i analizy. Oto kilka realnych przykładów, gdzie się przyda:
- W sklepie internetowym: pogrupować zamówienia według użytkownika, żeby pokazać historię zakupów każdego.
- Na platformie edukacyjnej: pogrupować studentów według nauczyciela albo przedmiotu.
- W aplikacji HR: pogrupować pracowników według działu i policzyć headcount.
- Na rozmowie kwalifikacyjnej: mogą poprosić o napisanie grupowania realnych danych z agregacją wartości.
LINQ pozwala bardzo szybko prototypować takie zadania, a umiejętność czytania i pisania grupowań — to duży plus dla każdego .NET-developera.
Wizualny schemat
Oto prosta blokowa schematka przetwarzania kolekcji metodą GroupBy:
[Kolekcja studentów]
|
[GroupBy po ClassName]
|
[Grupa "9A"] ---> [Jan, Olek]
[Grupa "10B"] --> [Maria, Svetlana]
Każda grupa — osobna mini-kolekcja z kluczem.
6. Częste błędy i pułapki
Niektórzy studenci na początku się gubią przez specyfikę wyniku GroupBy:
- Po wywołaniu GroupBy nie dostaniesz po prostu "grup" typu List<List<Student>>, tylko kolekcję IGrouping<TKey, TElement>. Do pracy z grupami używaj właściwości Key i iteracji po elementach grupy.
- Grupowanie po złożonym kluczu (kilka pól): nie zapomnij użyć anonimowego typu albo stworzyć osobną klasę, jeśli potrzebujesz strukturalnego klucza.
- Jeśli chcesz znać tylko ilość elementów w grupach — nie zapomnij użyć Select(g => g.Count()), inaczej będziesz musiał ręcznie liczyć elementy w każdej grupie.
- Czasem można się pogubić z zagnieżdżeniem pętli: pierwsza pętla po grupach, druga — po elementach w grupie.
- Jeśli pomylisz się z lambdą albo kluczem, dostaniesz nie te grupy, których się spodziewałeś (albo jedną wielką grupę, jeśli klucz jest taki sam dla wszystkich).
GO TO FULL VERSION