Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 47 z 58

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.

C# polimorfizm virtual override is as switch po typach 90 min
CEL LEKCJI

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() i switch po typach oraz uzasadnisz wybór.
  • Wskażesz sytuacje, w których sprawdzanie typu jest błędem projektowym.
TEORIA

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:

jedna klasa, rozgałęzienie w środku
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ć:

hierarchia — bez rozgałęzień
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.

TEORIA

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:

PytanieKto odpowiadaKiedyNa podstawie czego
Czy wolno wywołać tę metodę?kompilatorprzy kompilacjityp zmiennej
Która wersja metody się wykona?środowisko uruchomieniowew trakcie działaniatyp obiektu

Widać to na jednym przykładzie:

Program.cs
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
wynik w konsoli
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

sprawdź sam, potem uruchom
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?
wynik w konsoli
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ą.

TEORIA

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.

Program.cs
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));
wynik w konsoli
--- 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:

Program.cs
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:

Program.cs
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");
TEORIA

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:

definicje typó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:

Program.cs
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");
wynik w konsoli
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.

TEORIA

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

Program.cs
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

Program.cs
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");
ZapisGdy typ pasujeGdy typ nie pasuje
(Warsztat)azwraca obiektInvalidCastException — program przerywa
a as Warsztatzwraca obiektzwraca null
a is Warsztat wtrue, zmienna w gotowafalse, idziemy dalej

3. GetType() i typeof — dokładnie ten typ

Program.cs
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:

Program.cs
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}");
}
wynik w konsoli
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

SytuacjaCo zrobić
Każda klasa pochodna robi to samo, tylko inaczejmetoda virtual — nie sprawdzaj typu
Ten sam switch po typach w kilku metodachprzenieś 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 plikurozgałęzienie w metodzie fabrykującej — też uczciwe
Wypisanie obiektu w konsoliprzesłoń ToString()
PRZYKŁAD

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 — pełny plik
// 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}");
    }
}
wynik w konsoli
=== 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(), Czas i Cena na zmiennej typu Atrakcja. Metoda i właściwość są virtual, więc za każdym razem działa wersja klasy obiektu — dlatego cztery wiersze są różne.
  • Cena turnieju liczy się z liczby uczestników: 16 to więcej niż 8, więc wychodzi 5 zł. Razem 5 + 10 + 0 + 0 = 15 zł.
  • Zapowiedz nie zna żadnej klasy pochodnej. Podaje obiekt do interpolacji, co wywołuje ToString() z klasy bazowej — a ta w środku odczytuje Cena, czyli znowu wersję klasy pochodnej. Jedna metoda ToString obsługuje pięć klas.
  • GetType().Name pokazuje prawdziwą klasę obiektu, mimo że zmienna ma typ bazowy.
  • Pętla „tylko warsztaty” używa is, bo Sala i WolneMiejsca istnieją wyłącznie w klasie Warsztat. Pozostałe trzy atrakcje po prostu nie wchodzą do if.
  • Ostatni blok dodaje klasę Konkurs. Metoda Zapowiedz obsługuje ją bez żadnej zmiany — nie wiedziała o jej istnieniu, gdy ją pisaliśmy.
  • Pokaz nie przesłania Cena, więc bierze 0 zł z klasy bazowej. Przesłaniamy tylko to, co się różni.

Sprawdź trzy rzeczy

  1. Usuń override z Cena w klasie Turniej. Suma spadnie do 10 zł — polimorfizm przestał działać dla tej jednej właściwości, a program kompiluje się bez błędu.
  2. Dopisz własną klasę PokazLaserow : Pokaz z przesłoniętym Przedstaw() i dodaj ją do listy. Sprawdź, ile linii programu musiałeś zmienić poza samą klasą.
  3. Zamień w Zapowiedz parametr Atrakcja a na Warsztat a. Kompilator odrzuci trzy z pięciu wywołań — to pokazuje, po co typ bazowy w parametrze.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
polimorfizmjedno wywołanie, wiele zachowańWymaga dziedziczenia oraz pary virtual / override.
typ zmiennejdecyduje, co wolno wywołaćSprawdza kompilator. Węższy typ — mniej dostępnych składowych.
typ obiektudecyduje, która wersja się wykonaSprawdzane w trakcie działania programu.
virtual przy właściwościwłaściwość do przesłonięciaPrzesłonięcie musi mieć te same akcesory (CS0546).
metoda z parametrem typu bazowegoobsługuje wszystkie klasy pochodneTakże te napisane później. Główny zysk z polimorfizmu.
metoda zwracająca typ bazowyoddaje obiekt klasy pochodnejWzorzec metody fabrykującej.
a is Warsztat wsprawdzenie typu z utworzeniem zmiennejUwzględnia całą hierarchię.
a as Warsztatrzutowanie zwracające nullWynik jest typem ? — trzeba sprawdzić.
a.GetType()klasa obiektu.Name daje samą nazwę bez przestrzeni nazw.
typeof(Warsztat)klasa podana z nazwyPorównanie GetType() == typeof(...) jest dokładne.
switch po typachrozgałęzienie po klasie obiektuTypy pochodne przed bazowymi, _ na końcu.
when w gałęzi switchdodatkowy warunekSprawdzany po dopasowaniu typu.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Zapomniany override w jednej klasie pochodnejNie 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 virtualProgram 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 AtrakcjaCS1061. Typ zmiennej nie ma tej składowej. Najpierw is, potem odczyt.
(Warsztat)a bez sprawdzeniaInvalidCastException przy pierwszym obiekcie innej klasy. Użyj is albo as.
as bez sprawdzenia wynikuNullReferenceException przy odczycie. Wynik as zawsze trzeba porównać z null albo użyć ?.
Dopisanie set w przesłoniętej właściwościCS0546 — brak dostępnego akcesora set w wersji bazowej.
Gałąź z typem bazowym na początku switchCS8510 — 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 bazowymWykona się wersja pochodna, zanim jej pola dostaną wartości — łatwo dostać null albo zero.
Klasa bazowa bez sensownej wersji domyślnejJeśli wersja bazowa nie ma co robić, prawdopodobnie powinna być abstract — to temat lekcji 47.
ZADANIA

Zadania

ZAD 1Trzy figury, jedna pętla★☆☆

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.

ZAD 2Metoda z parametrem typu bazowego★☆☆

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.

ZAD 3Właściwość wirtualna★★☆

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.

ZAD 4Fabryka obiektów★★☆

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.

ZAD 5Sprawdzanie typu — cztery sposoby★★☆

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

ZAD 6Cennik biletów bez rozgałęzień★★★

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.

PODSUMOWANIE

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ę if albo switch po 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.
  • virtual dział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.
  • is pyta „czy da się użyć jako”, GetType() == typeof(...) pyta „czy to dokładnie ta klasa”.
  • as zwraca null zamiast przerywać program, ale wynik trzeba sprawdzić.
  • W switch po typach gałęzie z typami pochodnymi idą przed bazowymi.
  • Zapomniany override nie jest błędem, tylko ostrzeżeniem — dlatego warto czytać ostrzeżenia.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.