Lekcja 17. Klasy generyczne (szablony klas)
Trudny / egzaminacyjnyPo co się tego uczymy?
Generyki to mechanizm, z którego korzystałeś już wielokrotnie (List, Dictionary), nie wiedząc jeszcze, jak dokładnie działa — ta lekcja odsłania tę "kuchnię" i pokazuje, jak tworzyć własne, elastyczne klasy działające dla dowolnego typu danych, bez powielania kodu.
Teoria
W Lekcji 8 korzystałeś z List<string> i Dictionary<string, int> — te "ostre nawiasy" z typem w środku to GENERYKI (generics), mechanizm, który teraz nauczysz się tworzyć SAMODZIELNIE.
Klasa generyczna to szablon klasy, który działa dla RÓŻNYCH typów danych, bez pisania osobnej wersji klasy dla każdego typu z osobna. Zamiast konkretnego typu (np. int), w definicji klasy używasz "placeholdera" — zwyczajowo nazywanego T (od "Type"):
class Pojemnik<T> { private T zawartosc; public void Ustaw(T wartosc) { zawartosc = wartosc; } public T Pobierz() { return zawartosc; } }
Tej samej klasy Pojemnik<T> możesz użyć dla DOWOLNEGO typu, podając go przy tworzeniu obiektu:
Pojemnik<int> pojemnikLiczb = new Pojemnik<int>();
Pojemnik<string> pojemnikTekstow = new Pojemnik<string>();
Dlaczego to lepsze niż kopiowanie klasy dla każdego typu? Bez generyków musiałbyś napisać osobno PojemnikInt, PojemnikString, PojemnikFilm — ten sam kod, powtórzony wielokrotnie, tylko z innym typem. Generyki pozwalają napisać logikę RAZ i używać jej dla dowolnego typu, zachowując przy tym pełne bezpieczeństwo typów (kompilator nie pozwoli wstawić string do Pojemnik<int>).
Dokładnie w ten sposób zbudowane są List<T> i Dictionary<TKey, TValue>, których używałeś już wcześniej — teraz wiesz, jak działają "od środka" i potrafisz stworzyć własny, podobny mechanizm.
Przykład z życia
Uniwersalne pudełko transportowe w magazynie może pomieścić różne rodzaje towarów (książki, elektronikę, ubrania) — sam mechanizm pakowania i transportu jest identyczny, zmienia się tylko to, CO jest w środku. To dokładnie idea klasy generycznej.
Kod (C#)
using System;
class Para<T1, T2> // klasa generyczna z DWOMA parametrami typu
{
public T1 Pierwszy;
public T2 Drugi;
public Para(T1 pierwszy, T2 drugi)
{
Pierwszy = pierwszy;
Drugi = drugi;
}
public void Wyswietl()
{
Console.WriteLine("(" + Pierwszy + ", " + Drugi + ")");
}
}
class Program
{
static void Main()
{
// ta sama klasa Para<T1, T2> działa dla RÓŻNYCH kombinacji typów
Para<string, int> filmOcena = new Para<string, int>("Diuna", 9);
filmOcena.Wyswietl(); // (Diuna, 9)
Para<string, double> produktCena = new Para<string, double>("Bilet", 24.99);
produktCena.Wyswietl(); // (Bilet, 24.99)
Para<int, int> wspolrzedne = new Para<int, int>(10, 20);
wspolrzedne.Wyswietl(); // (10, 20)
}
}
Komentarz i wyjaśnienie kodu
class Para<T1, T2> deklaruje klasę generyczną z DWOMA niezależnymi parametrami typu — przy tworzeniu obiektu (new Para<string, int>(...)) podajesz konkretne typy, którymi T1 i T2 zostaną zastąpione dla TEGO KONKRETNEGO obiektu. Kompilator pilnuje zgodności typów — próba filmOcena.Pierwszy = 5; (liczba zamiast tekstu) zgłosiłaby błąd kompilacji, bo T1 zostało ustalone jako string.
Ćwiczenie samodzielne
Zdefiniuj prostą klasę generyczną Pojemnik<T> z metodami Ustaw(T wartosc) i Pobierz(), i użyj jej dla typu int oraz string.
Plac zabaw — wypróbuj online
Poniższy kod wykonuje się od razu w Twojej przeglądarce — nic nie trzeba instalować. Zmień kod i kliknij „Uruchom”.
Zadania do pracy własnej
Utwórz klasę generyczną
Para<T>(z JEDNYM parametrem typu) przechowującą dwie wartości TEGO SAMEGO typu i metodęZamien()zamieniającą je miejscami.Zdefiniuj generyczną klasę
StosProsty<T>(uproszczony stos) z wewnętrznąList<T>, metodamiDodaj(T element)(dokłada na wierzch) iZdejmij()(usuwa i zwraca ostatnio dodany element).Zbuduj generyczną klasę
MagazynDanych<T>przechowującą listę elementów typuT, z metodami:Dodaj(T element),UsunPierwszy(),Liczba()zwracającą aktualną liczbę elementów, orazWyswietlWszystkie()wypisującą całą zawartość. Przetestuj tę samą klasę dla typustring(magazyn tytułów filmów) i dla własnej klasyProduktz wcześniejszych lekcji.
Typowe błędy
Częsty błąd to próba wykonania na parametrze typu T operacji, których nie każdy typ obsługuje (np. T + T nie zadziała, bo nie każdy typ da się dodawać operatorem +) — generyki bez dodatkowych ograniczeń (constraints, temat wykraczający poza ten kurs) muszą działać dla KAŻDEGO możliwego typu. Drugi błąd: mylenie nazwy parametru typu T z prawdziwą, konkretną klasą — T to tylko "placeholder", zastępowany dopiero przy użyciu klasy generycznej. Trzeci: tworzenie osobnych, niemal identycznych klas dla różnych typów zamiast jednej klasy generycznej — to dokładnie problem, który generyki mają rozwiązać.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja efektu INF.04.4.8 z podstawy programowej: "programuje szablony (wzorce) klas" — dosłowne sformułowanie oficjalnego tekstu to "generyki dla typów liczbowych", co ten przykład (i zadania) w pełni pokrywa.