Polimorfizm — to samo polecenie, różne wykonania
Organizator uruchamia wszystkie atrakcje, nie sprawdzając przy każdej, jakiego jest rodzaju. Konkretna atrakcja zna własne zachowanie.
Czego się dziś nauczysz
- Rozpoznasz w kodzie powtarzające się rozgałęzienie po rodzaju i zamienisz je na hierarchię klas.
- Rozdzielisz dwie decyzje: co wolno wywołać (typ zmiennej) i która wersja się wykona (typ obiektu).
- Napiszesz metodę z parametrem typu bazowego, która obsłuży klasy pochodne napisane później.
- Przesłonisz właściwość wirtualną i wyjaśnisz, dlaczego akcesory muszą się zgadzać.
- Użyjesz
is,as,GetType()iswitchpo typach oraz uzasadnisz wybór. - Wskażesz sytuacje, w których sprawdzanie typu jest błędem projektowym.
Rozgałęzienie, które rośnie z każdym rodzajem
Na dniu otwartym szkoły są trzy rodzaje atrakcji: turniej gier, warsztat i pokaz. Każdy przedstawia się inaczej. Pierwszy pomysł, jaki przychodzi do głowy, to jedna klasa z polem „rodzaj” i rozgałęzieniem:
class Atrakcja
{
public string Nazwa { get; }
public string Rodzaj { get; } // "turniej", "warsztat", "pokaz"
public int Czas { get; }
public Atrakcja(string nazwa, string rodzaj, int czas)
{
Nazwa = nazwa;
Rodzaj = rodzaj;
Czas = czas;
}
public void Przedstaw()
{
if (Rodzaj == "turniej")
{
Console.WriteLine($"{Nazwa} - turniej, zapisy parami");
}
else if (Rodzaj == "warsztat")
{
Console.WriteLine($"{Nazwa} - warsztat, limit miejsc");
}
else if (Rodzaj == "pokaz")
{
Console.WriteLine($"{Nazwa} - pokaz, wejscie wolne");
}
else
{
Console.WriteLine($"{Nazwa} - nieznany rodzaj");
}
}
}
To działa. Problem widać, gdy dojdzie czwarta atrakcja — konkurs plastyczny. Trzeba znaleźć każde miejsce, w którym program pyta o rodzaj, i dopisać tam gałąź. A pyta w wielu miejscach: przy przedstawianiu, przy liczeniu ceny biletu, przy zapisach, przy wydruku planu. Jedno przeoczone miejsce nie zgłosi błędu — po prostu nowa atrakcja zachowa się jak „nieznany rodzaj”.
Objaw, który warto rozpoznawać
Jeśli w kilku metodach powtarza się ten sam zestaw gałęzi if albo case, sprawdzający to samo pole, to znak, że zamiast jednej klasy z rodzajem powinna być klasa bazowa i klasy pochodne.
Po przebudowie na hierarchię rozgałęzienie znika, a każda atrakcja sama wie, co ma powiedzieć:
class Atrakcja
{
public string Nazwa { get; }
public int Czas { get; }
public Atrakcja(string nazwa, int czas)
{
Nazwa = nazwa;
Czas = czas;
}
public virtual void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min)");
}
}
class Turniej : Atrakcja
{
public Turniej(string nazwa, int czas) : base(nazwa, czas) { }
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - turniej, zapisy parami");
}
}
class Warsztat : Atrakcja
{
public Warsztat(string nazwa, int czas) : base(nazwa, czas) { }
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - warsztat, limit miejsc");
}
}
Czwarta atrakcja to teraz nowy plik z klasą. Nie trzeba otwierać ani jednej metody, która już działa. To jest cały sens polimorfizmu.
Dwie decyzje w dwóch różnych momentach
W lekcji 45 wystarczyło nam stwierdzenie: „z virtual decyduje typ obiektu”. Teraz rozłóżmy to na dwie osobne decyzje, bo dopiero wtedy przestaje to być magią.
Przy każdym wywołaniu metody odpowiedź dają dwa różne mechanizmy w dwóch różnych momentach:
| Pytanie | Kto odpowiada | Kiedy | Na podstawie czego |
|---|---|---|---|
| Czy wolno wywołać tę metodę? | kompilator | przy kompilacji | typ zmiennej |
| Która wersja metody się wykona? | środowisko uruchomieniowe | w trakcie działania | typ obiektu |
Widać to na jednym przykładzie:
Atrakcja a = new Warsztat("Druk 3D", 90, "sala 12");
Console.WriteLine(a.GetType().Name); // Warsztat - to jest typ OBIEKTU
a.Przedstaw(); // wykona sie wersja z klasy Warsztat
// a.Sala // BLAD CS1061 - typ ZMIENNEJ to Atrakcja,
// a Atrakcja nie ma wlasciwosci Sala
Warsztat
Druk 3D (90 min) - warsztat, sala 12
Obiekt jest warsztatem i przez cały czas nim pozostaje. Zmienna typu Atrakcja nie zmienia obiektu — zmienia tylko to, co wolno przez nią zobaczyć. Dobrym porównaniem jest okienko: przez węższe okno widać mniej, ale za nim stoi ten sam przedmiot.
Jak środowisko wybiera wersję
Każdy obiekt zawiera ukrytą informację o swojej klasie. Klasa ma tablicę metod wirtualnych — spis, która wersja obowiązuje. Wywołanie metody virtual to zajrzenie do tego spisu w trakcie działania, dlatego działa nawet dla klasy, która powstała po napisaniu pętli. Nie musisz tego pamiętać, ale warto wiedzieć, że nie ma tu żadnego zgadywania.
Trzy pytania kontrolne
Atrakcja x = new Turniej("Retro", 60);
Turniej y = new Turniej("Retro", 60);
Atrakcja z = new Atrakcja("Przerwa", 15);
x.Przedstaw(); // 1. ktora wersja?
y.Przedstaw(); // 2. ktora wersja?
z.Przedstaw(); // 3. ktora wersja?
Retro (60 min) - turniej, zapisy parami
Retro (60 min) - turniej, zapisy parami
Przerwa (15 min)
Dwa pierwsze wywołania są identyczne w skutku, choć zmienne mają różne typy — bo obiekt w obu przypadkach jest turniejem. Trzecie wywołuje wersję bazową, bo obiekt naprawdę jest zwykłą atrakcją.
Typ bazowy w parametrze metody
Największy zysk z polimorfizmu widać nie w pętli, a w parametrach metod. Metoda, która przyjmuje typ bazowy, obsługuje wszystkie klasy pochodne — także te, które powstaną później.
static void Zapowiedz(Atrakcja a)
{
Console.WriteLine("--- nastepny punkt programu ---");
a.Przedstaw();
}
Zapowiedz(new Turniej("Retro", 60));
Zapowiedz(new Warsztat("Druk 3D", 90, "sala 12"));
Zapowiedz(new Atrakcja("Przerwa", 15));
--- nastepny punkt programu ---
Retro (60 min) - turniej, zapisy parami
--- nastepny punkt programu ---
Druk 3D (90 min) - warsztat, sala 12
--- nastepny punkt programu ---
Przerwa (15 min)
Metoda Zapowiedz jest napisana raz i nie zna nazwy żadnej klasy pochodnej. Dokładnie tak działa Console.WriteLine(object): przyjmuje object, więc przyjmuje wszystko, i woła ToString(), która w Twojej klasie może być przesłonięta.
Typ bazowy jako wynik metody
Metoda może też zwracać typ bazowy, a naprawdę oddać obiekt klasy pochodnej:
static Atrakcja Utworz(string rodzaj)
{
if (rodzaj == "turniej")
{
return new Turniej("Retro", 60);
}
if (rodzaj == "warsztat")
{
return new Warsztat("Druk 3D", 90, "sala 12");
}
return new Atrakcja("Punkt programu", 15);
}
Atrakcja a = Utworz("warsztat");
a.Przedstaw(); // wersja warsztatu
Jedno rozgałęzienie zostaje — i to jest w porządku
Zauważ, że if po rodzaju nie zniknął całkowicie: przeniósł się do jednego miejsca, które tworzy obiekty. Tam jest nieunikniony — ktoś musi zdecydować, co utworzyć na podstawie tekstu z pliku albo z konsoli. Ważne jest to, że po utworzeniu nikt już nie pyta o rodzaj. Taka metoda nazywa się fabryką.
Kolekcja typu bazowego
To samo dotyczy List<T> z lekcji 31 — lista typu bazowego przyjmuje wszystkie klasy pochodne:
List<Atrakcja> plan = new List<Atrakcja>();
plan.Add(new Turniej("Retro", 60));
plan.Add(new Warsztat("Druk 3D", 90, "sala 12"));
plan.Add(new Atrakcja("Przerwa", 15));
int razem = 0;
foreach (Atrakcja a in plan)
{
a.Przedstaw();
razem += a.Czas;
}
Console.WriteLine($"Razem: {razem} min");
Przesłanianie właściwości
Przesłaniać można nie tylko metody. virtual wolno postawić także przy właściwości — i bardzo często to właśnie jest najkrótsze rozwiązanie.
Każda atrakcja ma cenę wstępu. Domyślnie wstęp jest wolny, ale warsztat kosztuje, a turniej ma wpisowe zależne od liczby uczestników:
class Atrakcja
{
public string Nazwa { get; }
public int Czas { get; }
public Atrakcja(string nazwa, int czas)
{
Nazwa = nazwa;
Czas = czas;
}
public virtual decimal Cena => 0m; // wstep wolny
public virtual string Info => "wejscie bez zapisow";
}
class Warsztat : Atrakcja
{
public string Sala { get; }
public Warsztat(string nazwa, int czas, string sala) : base(nazwa, czas)
{
Sala = sala;
}
public override decimal Cena => 10m;
public override string Info => $"sala {Sala}, zapisy u prowadzacego";
}
class Turniej : Atrakcja
{
public int Uczestnicy { get; }
public Turniej(string nazwa, int czas, int uczestnicy) : base(nazwa, czas)
{
Uczestnicy = uczestnicy;
}
public override decimal Cena => Uczestnicy > 8 ? 5m : 8m;
public override string Info => $"{Uczestnicy} uczestnikow, zapisy parami";
}
Teraz wypisanie cennika nie wymaga ani jednego pytania o rodzaj:
Atrakcja[] plan =
{
new Turniej("Retro", 60, 16),
new Warsztat("Druk 3D", 90, "sala 12"),
new Atrakcja("Przerwa", 15)
};
decimal suma = 0m;
foreach (Atrakcja a in plan)
{
Console.WriteLine($"{a.Nazwa,-12} {a.Cena,6:F2} zl {a.Info}");
suma += a.Cena;
}
Console.WriteLine($"{"RAZEM",-12} {suma,6:F2} zl");
Retro 5,00 zl 16 uczestnikow, zapisy parami
Druk 3D 10,00 zl sala 12, zapisy u prowadzacego
Przerwa 0,00 zl wejscie bez zapisow
RAZEM 15,00 zl
Ograniczenie przesłaniania właściwości
Przesłonięcie musi mieć te same akcesory co wersja bazowa. Jeśli w klasie bazowej właściwość jest tylko do odczytu ({ get; } albo =>), to w klasie pochodnej nie da się dopisać set — kompilator zgłosi CS0546. Gdy klasy pochodne mają zapisywać wartość, setter musi być już w klasie bazowej.
Gdy naprawdę trzeba sprawdzić typ
Czasem naprawdę trzeba sprawdzić, jakiej klasy jest obiekt — na przykład gdy tylko warsztat ma listę zapisanych uczniów. C# daje na to cztery narzędzia. Poznaj wszystkie, ale używaj ich rzadko: każde sprawdzenie typu to rozgałęzienie, które polimorfizm miał usunąć.
1. is ze zmienną — najczęstszy wybór
foreach (Atrakcja a in plan)
{
if (a is Warsztat w) // sprawdz i od razu utworz zmienna typu Warsztat
{
Console.WriteLine($"{w.Nazwa}: sala {w.Sala}");
}
}
2. as — rzutowanie, które nie przerywa programu
Atrakcja a = plan[1];
Warsztat? w = a as Warsztat; // null, jesli obiekt nie jest warsztatem
if (w != null)
{
Console.WriteLine(w.Sala);
}
// krocej, z operatorami z lekcji 34:
Console.WriteLine((a as Warsztat)?.Sala ?? "to nie warsztat");
| Zapis | Gdy typ pasuje | Gdy typ nie pasuje |
|---|---|---|
(Warsztat)a | zwraca obiekt | InvalidCastException — program przerywa |
a as Warsztat | zwraca obiekt | zwraca null |
a is Warsztat w | true, zmienna w gotowa | false, idziemy dalej |
3. GetType() i typeof — dokładnie ten typ
Atrakcja a = new Warsztat("Druk 3D", 90, "sala 12");
Console.WriteLine(a.GetType().Name); // Warsztat
Console.WriteLine(a is Atrakcja); // True - warsztat JEST atrakcja
Console.WriteLine(a.GetType() == typeof(Atrakcja)); // False - ale nie jest DOKLADNIE atrakcja
Console.WriteLine(a.GetType() == typeof(Warsztat)); // True
Różnica, którą łatwo pomylić
is pyta „czy da się użyć jako…” i uwzględnia całą hierarchię. GetType() == typeof(...) pyta „czy to dokładnie ta klasa” i odpowiada false dla klas pochodnych. W zadaniach szkolnych prawie zawsze chcesz is.
4. switch po typach
Wyrażenie switch z lekcji 15 potrafi rozgałęziać się po typie:
foreach (Atrakcja a in plan)
{
string uwaga = a switch
{
Warsztat w when w.Sala == "sala 12" => "sala komputerowa - zdejmij plecak",
Warsztat w => $"idz do {w.Sala}",
Turniej t => $"zglos sie parami, miejsc: {t.Uczestnicy}",
_ => "brak uwag"
};
Console.WriteLine($"{a.Nazwa}: {uwaga}");
}
Retro: zglos sie parami, miejsc: 16
Druk 3D: sala komputerowa - zdejmij plecak
Przerwa: brak uwag
Kolejność gałęzi ma znaczenie
Gałąź Atrakcja a postawiona jako pierwsza przechwyci wszystkie obiekty, bo każda atrakcja pasuje do typu bazowego — reszta gałęzi stanie się nieosiągalna (ostrzeżenie CS8510). Typy pochodne wymieniaj przed bazowymi, a _ zawsze na końcu.
Kiedy sprawdzanie typu jest błędem
| Sytuacja | Co zrobić |
|---|---|
| Każda klasa pochodna robi to samo, tylko inaczej | metoda virtual — nie sprawdzaj typu |
Ten sam switch po typach w kilku metodach | przenieś zachowanie do klas jako metodę virtual |
| Tylko jedna klasa pochodna ma dodatkową składową | is ze zmienną — to uczciwe użycie |
| Trzeba utworzyć obiekt na podstawie tekstu z pliku | rozgałęzienie w metodzie fabrykującej — też uczciwe |
| Wypisanie obiektu w konsoli | przesłoń ToString() |
Plan dnia otwartego
Program planu dnia otwartego. Pięć klas, ani jednego pytania o rodzaj w pętlach. Na końcu dochodzi piąta atrakcja — zwróć uwagę, że żadna wcześniejsza linia nie została zmieniona.
// Program.cs - pelny plik
List<Atrakcja> plan = new List<Atrakcja>
{
new Turniej("Turniej retro", 60, 16),
new Warsztat("Druk 3D", 90, "sala 12"),
new Pokaz("Roboty na scenie", 30),
new Atrakcja("Przerwa obiadowa", 45)
};
Console.WriteLine("=== plan dnia otwartego ===");
int minuty = 0;
decimal kasa = 0m;
foreach (Atrakcja a in plan)
{
a.Przedstaw();
minuty += a.Czas;
kasa += a.Cena;
}
Console.WriteLine();
Console.WriteLine($"Czas razem: {minuty} min");
Console.WriteLine($"Wpisowe razem: {kasa:F2} zl");
Console.WriteLine();
Console.WriteLine("=== zapowiedzi przez wspolna metode ===");
foreach (Atrakcja a in plan)
{
Zapowiedz(a);
}
Console.WriteLine();
Console.WriteLine("=== tylko warsztaty ===");
foreach (Atrakcja a in plan)
{
if (a is Warsztat w)
{
Console.WriteLine($"{w.Nazwa}: {w.Sala}, wolnych miejsc {w.WolneMiejsca}");
}
}
Console.WriteLine();
Console.WriteLine("=== nowy rodzaj bez zmiany petli ===");
plan.Add(new Konkurs("Konkurs plastyczny", 40, "nagroda ksiazkowa"));
Zapowiedz(plan[plan.Count - 1]);
static void Zapowiedz(Atrakcja a)
{
Console.WriteLine($"[{a.GetType().Name,-9}] {a} | {a.Info}");
}
class Atrakcja
{
public string Nazwa { get; }
public int Czas { get; }
public Atrakcja(string nazwa, int czas)
{
Nazwa = nazwa;
Czas = czas;
}
public virtual decimal Cena => 0m;
public virtual string Info => "wejscie wolne, bez zapisow";
public virtual void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min)");
}
public override string ToString()
{
return $"{Nazwa}, {Czas} min, {Cena:F2} zl";
}
}
class Turniej : Atrakcja
{
public int Uczestnicy { get; }
public Turniej(string nazwa, int czas, int uczestnicy) : base(nazwa, czas)
{
Uczestnicy = uczestnicy;
}
public override decimal Cena => Uczestnicy > 8 ? 5m : 8m;
public override string Info => $"{Uczestnicy} uczestnikow, zapisy parami";
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - turniej dla {Uczestnicy} osob");
}
}
class Warsztat : Atrakcja
{
public string Sala { get; }
public int WolneMiejsca { get; private set; }
public Warsztat(string nazwa, int czas, string sala) : base(nazwa, czas)
{
Sala = sala;
WolneMiejsca = 12;
}
public override decimal Cena => 10m;
public override string Info => $"sala {Sala}, zapisy u prowadzacego";
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - warsztat, {Sala}");
}
public void Zapisz(string uczen)
{
if (WolneMiejsca == 0)
{
Console.WriteLine($"{Nazwa}: brak miejsc dla {uczen}");
return;
}
WolneMiejsca--;
}
}
class Pokaz : Atrakcja
{
public Pokaz(string nazwa, int czas) : base(nazwa, czas) { }
public override string Info => "duza sala gimnastyczna, wejscie wolne";
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - pokaz na scenie");
}
}
class Konkurs : Atrakcja
{
public string Nagroda { get; }
public Konkurs(string nazwa, int czas, string nagroda) : base(nazwa, czas)
{
Nagroda = nagroda;
}
public override string Info => $"nagroda: {Nagroda}";
public override void Przedstaw()
{
Console.WriteLine($"{Nazwa} ({Czas} min) - konkurs, {Nagroda}");
}
}
=== plan dnia otwartego ===
Turniej retro (60 min) - turniej dla 16 osob
Druk 3D (90 min) - warsztat, sala 12
Roboty na scenie (30 min) - pokaz na scenie
Przerwa obiadowa (45 min)
Czas razem: 225 min
Wpisowe razem: 15,00 zl
=== zapowiedzi przez wspolna metode ===
[Turniej ] Turniej retro, 60 min, 5,00 zl | 16 uczestnikow, zapisy parami
[Warsztat ] Druk 3D, 90 min, 10,00 zl | sala 12, zapisy u prowadzacego
[Pokaz ] Roboty na scenie, 30 min, 0,00 zl | duza sala gimnastyczna, wejscie wolne
[Atrakcja ] Przerwa obiadowa, 45 min, 0,00 zl | wejscie wolne, bez zapisow
=== tylko warsztaty ===
Druk 3D: sala 12, wolnych miejsc 12
=== nowy rodzaj bez zmiany petli ===
[Konkurs ] Konkurs plastyczny, 40 min, 0,00 zl | nagroda: nagroda ksiazkowa
Co dzieje się po kolei
- Pierwsza pętla wywołuje
Przedstaw(),CzasiCenana zmiennej typuAtrakcja. Metoda i właściwość sąvirtual, więc za każdym razem działa wersja klasy obiektu — dlatego cztery wiersze są różne. Cenaturnieju liczy się z liczby uczestników: 16 to więcej niż 8, więc wychodzi 5 zł. Razem 5 + 10 + 0 + 0 = 15 zł.Zapowiedznie zna żadnej klasy pochodnej. Podaje obiekt do interpolacji, co wywołujeToString()z klasy bazowej — a ta w środku odczytujeCena, czyli znowu wersję klasy pochodnej. Jedna metodaToStringobsługuje pięć klas.GetType().Namepokazuje prawdziwą klasę obiektu, mimo że zmienna ma typ bazowy.- Pętla „tylko warsztaty” używa
is, boSalaiWolneMiejscaistnieją wyłącznie w klasieWarsztat. Pozostałe trzy atrakcje po prostu nie wchodzą doif. - Ostatni blok dodaje klasę
Konkurs. MetodaZapowiedzobsługuje ją bez żadnej zmiany — nie wiedziała o jej istnieniu, gdy ją pisaliśmy. Pokaznie przesłaniaCena, więc bierze 0 zł z klasy bazowej. Przesłaniamy tylko to, co się różni.
Sprawdź trzy rzeczy
- Usuń
overridezCenaw klasieTurniej. Suma spadnie do 10 zł — polimorfizm przestał działać dla tej jednej właściwości, a program kompiluje się bez błędu. - Dopisz własną klasę
PokazLaserow : Pokazz przesłoniętymPrzedstaw()i dodaj ją do listy. Sprawdź, ile linii programu musiałeś zmienić poza samą klasą. - Zamień w
ZapowiedzparametrAtrakcja anaWarsztat a. Kompilator odrzuci trzy z pięciu wywołań — to pokazuje, po co typ bazowy w parametrze.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
| polimorfizm | jedno wywołanie, wiele zachowań | Wymaga dziedziczenia oraz pary virtual / override. |
| typ zmiennej | decyduje, co wolno wywołać | Sprawdza kompilator. Węższy typ — mniej dostępnych składowych. |
| typ obiektu | decyduje, która wersja się wykona | Sprawdzane w trakcie działania programu. |
virtual przy właściwości | właściwość do przesłonięcia | Przesłonięcie musi mieć te same akcesory (CS0546). |
| metoda z parametrem typu bazowego | obsługuje wszystkie klasy pochodne | Także te napisane później. Główny zysk z polimorfizmu. |
| metoda zwracająca typ bazowy | oddaje obiekt klasy pochodnej | Wzorzec metody fabrykującej. |
a is Warsztat w | sprawdzenie typu z utworzeniem zmiennej | Uwzględnia całą hierarchię. |
a as Warsztat | rzutowanie zwracające null | Wynik jest typem ? — trzeba sprawdzić. |
a.GetType() | klasa obiektu | .Name daje samą nazwę bez przestrzeni nazw. |
typeof(Warsztat) | klasa podana z nazwy | Porównanie GetType() == typeof(...) jest dokładne. |
switch po typach | rozgałęzienie po klasie obiektu | Typy pochodne przed bazowymi, _ na końcu. |
when w gałęzi switch | dodatkowy warunek | Sprawdzany po dopasowaniu typu. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Zapomniany override w jednej klasie pochodnej | Nie ma błędu, jest tylko ostrzeżenie CS0108 — ta jedna klasa cicho używa wersji bazowej. Objaw: „wszystko działa, ale jeden rodzaj zachowuje się domyślnie”. |
switch po typach zamiast metody virtual | Program działa, ale nowy typ wymaga odszukania wszystkich rozgałęzień. To ten sam problem, od którego zaczęliśmy lekcję. |
a.Sala na zmiennej typu Atrakcja | CS1061. Typ zmiennej nie ma tej składowej. Najpierw is, potem odczyt. |
(Warsztat)a bez sprawdzenia | InvalidCastException przy pierwszym obiekcie innej klasy. Użyj is albo as. |
as bez sprawdzenia wyniku | NullReferenceException przy odczycie. Wynik as zawsze trzeba porównać z null albo użyć ?. |
Dopisanie set w przesłoniętej właściwości | CS0546 — brak dostępnego akcesora set w wersji bazowej. |
Gałąź z typem bazowym na początku switch | CS8510 — dalsze gałęzie są nieosiągalne, bo pierwsza dopasowuje wszystko. |
GetType() == typeof(Atrakcja) jako „czy to atrakcja” | Daje false dla każdej klasy pochodnej. Do takiego pytania służy is. |
Metoda virtual wywołana w konstruktorze bazowym | Wykona się wersja pochodna, zanim jej pola dostaną wartości — łatwo dostać null albo zero. |
| Klasa bazowa bez sensownej wersji domyślnej | Jeśli wersja bazowa nie ma co robić, prawdopodobnie powinna być abstract — to temat lekcji 47. |
Zadania
Napisz klasę Figura z właściwością Nazwa ustawianą w konstruktorze oraz metodą virtual double Pole() zwracającą 0. Napisz klasy Kwadrat (pole a), Prostokat (pola a i b) i Kolo (pole r), każda z przesłoniętą metodą Pole(). Utwórz tablicę Figura[] z trzema obiektami i jedną pętlą foreach wypisz nazwę oraz pole każdej figury z dokładnością do dwóch miejsc. Na końcu wypisz sumę pól.
Do zadania 1 dopisz metodę static void Raport(Figura f), która wypisuje nazwę figury, jej pole i informację, czy pole jest większe od 50. Wywołaj ją dla wszystkich trzech figur. Następnie zmień typ parametru na Kwadrat i zapisz w komentarzu, ile wywołań przestało się kompilować i dlaczego.
Napisz klasę Pracownik z właściwościami Imie i Godziny oraz wirtualną właściwością StawkaGodzinowa zwracającą 25 zł i wirtualną właściwością Wyplata zwracającą Godziny * StawkaGodzinowa. Napisz klasy Stazysta (stawka 18 zł) i Kierownik (stawka 40 zł, a Wyplata dodaje 500 zł dodatku funkcyjnego, wykorzystując wartość z klasy bazowej). W programie utwórz tablicę pracowników, wypisz listę wypłat wyrównaną w kolumnach i podaj sumę. Sprawdź, co się stanie, gdy w klasie Kierownik usuniesz override tylko ze StawkaGodzinowa.
Do zadania 3 dopisz metodę static Pracownik Utworz(string kod, string imie, int godziny), która na podstawie kodu "S", "K" lub dowolnego innego zwraca odpowiednio stażystę, kierownika albo zwykłego pracownika. W programie wczytaj z konsoli trzy wiersze w formacie kod;imie;godziny, rozdziel je metodą Split, utwórz obiekty przez tę metodę i dodaj do List<Pracownik>. Wypisz listę wypłat. W komentarzu napisz, ile rozgałęzień po rodzaju pracownika ma cały program i dlaczego to jest w porządku.
Utwórz tablicę Figura[] z zadania 1, w której jest co najmniej jedno koło. Napisz cztery pętle, w których wypiszesz promień każdego koła, używając kolejno: is ze zmienną, as z porównaniem do null, rzutowania (Kolo) po sprawdzeniu if (f is Kolo) oraz wyrażenia switch po typach. Przy każdej pętli dopisz komentarz, kiedy ten sposób jest najwygodniejszy. Na końcu wypisz dla każdego elementu wynik f is Figura oraz f.GetType() == typeof(Figura) i wyjaśnij w komentarzu różnicę.
Napisz hierarchię biletów na szkolny turniej. Klasa bazowa Bilet ma Wlasciciel, wirtualną właściwość CenaPodstawowa (20 zł), wirtualną właściwość Znizka (0 procent), wyliczaną właściwość DoZaplaty zwracającą cenę po zniżce oraz przesłonięte ToString(). Napisz klasy BiletUczniowski (zniżka 50 procent), BiletNauczycielski (zniżka 25 procent) i BiletRodzinny (cena podstawowa 50 zł, zniżka 10 procent, dodatkowa właściwość LiczbaOsob). Utwórz listę co najmniej pięciu biletów, wypisz je jedną pętlą w formie tabelki i podaj łączną kwotę oraz łączną wartość udzielonych zniżek. Następnie dopisz klasę BiletVip (cena 100 zł, bez zniżki) i dodaj jeden taki bilet do listy. W komentarzu na końcu pliku wypisz, ile wierszy poza samą nową klasą i jej dodaniem do listy musiałeś zmienić — jeśli więcej niż zero, znajdź w programie rozgałęzienie po typie i zastąp je przesłonięciem.
Co trzeba zapamiętać
- Polimorfizm to jedno wywołanie i wiele zachowań; potrzebuje dziedziczenia oraz pary
virtual/override. - Typ zmiennej decyduje, co wolno wywołać. Typ obiektu decyduje, która wersja się wykona.
- Powtarzające się
ifalboswitchpo tym samym polu „rodzaj” to znak, że brakuje klas pochodnych. - Metoda z parametrem typu bazowego obsługuje też klasy, które powstaną po jej napisaniu.
virtualdziała także przy właściwościach; przesłonięcie musi mieć te same akcesory.- Jedno rozgałęzienie po rodzaju zwykle zostaje — w metodzie, która tworzy obiekty. Potem nikt już nie pyta o typ.
ispyta „czy da się użyć jako”,GetType() == typeof(...)pyta „czy to dokładnie ta klasa”.aszwracanullzamiast przerywać program, ale wynik trzeba sprawdzić.- W
switchpo typach gałęzie z typami pochodnymi idą przed bazowymi. - Zapomniany
overridenie jest błędem, tylko ostrzeżeniem — dlatego warto czytać ostrzeżenia.
Dokumentacja: Microsoft Learn — temat tej lekcji.