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

Klasy abstrakcyjne — wspólna baza z miejscem na szczegóły

W programie bilet może mieć wspólną nazwę, ale konkretny rodzaj biletu musi określić cenę. Sztuczna cena „0 dla każdego” mogłaby ukryć błąd.

C# abstract override metoda szablonowa CS0144 CS0534 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Rozpoznasz sytuację, w której klasa bazowa nie ma sensownej wartości domyślnej.
  • Zadeklarujesz abstract class i wyjaśnisz, dlaczego zmienna tego typu jest legalna, a new nie.
  • Napiszesz metodę i właściwość abstrakcyjną oraz przesłonisz je w klasie pochodnej.
  • Odróżnisz abstract od virtual jednym pytaniem: „musi” czy „może”.
  • Zbudujesz metodę szablonową — zwykłą metodę w bazie, która woła składowe abstrakcyjne.
  • Rozstrzygniesz, kiedy potrzebna jest klasa abstrakcyjna, a kiedy wystarczy zwykła klasa bazowa.
  • Rozpoznasz komunikaty CS0144, CS0534, CS0500, CS0503 i CS0513.

Przygotowanie: lekcje 01–46. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Wartość domyślna, której nie ma

Kasa szkolnego turnieju sprzedaje bilety uczniowskie, nauczycielskie i rodzinne. Każdy ma inną cenę. Zgodnie z lekcją 46 zrobimy klasę bazową i trzy pochodne:

klasa bazowa z wymyśloną ceną
class Bilet
{
    public string Wlasciciel { get; }

    public Bilet(string wlasciciel)
    {
        Wlasciciel = wlasciciel;
    }

    public virtual decimal Cena => 0m;      // ile ma tu byc???
}

class BiletUczniowski : Bilet
{
    public BiletUczniowski(string w) : base(w) { }
    public override decimal Cena => 10m;
}

I tu pojawia się kłopot. Jaka jest cena zwykłego biletu? Nie ma takiej ceny — „zwykły bilet” nie istnieje w kasie. Wpisaliśmy 0, bo trzeba było coś wpisać. Ta zerowa cena jest nieprawdą, która potem zrobi krzywdę:

skutek
List<Bilet> sprzedane = new List<Bilet>();
sprzedane.Add(new BiletUczniowski("Kasia"));
sprzedane.Add(new Bilet("Marek"));          // ktos utworzyl "zwykly" bilet

decimal kasa = 0m;
foreach (Bilet b in sprzedane)
{
    kasa += b.Cena;
}
Console.WriteLine($"Kasa: {kasa:F2} zl");   // 10,00 - a sprzedano dwa bilety

Program nie zgłosił błędu. Sprzedał bilet za zero złotych i policzył kasę, która nie zgadza się z liczbą wejść. Błąd wyjdzie dopiero przy rozliczeniu — jeśli ktoś je zrobi.

Gorzej: ta sama pułapka czeka na Ciebie przy każdej nowej klasie pochodnej. Jeśli za pół roku dopiszesz BiletVip i zapomnisz przesłonić Cena, bilety VIP zaczną być darmowe. Nic o tym nie powie.

Czego szukamy

Potrzebujemy klasy bazowej, która mówi dwie rzeczy: „nie wolno mnie utworzyć” oraz „każda klasa pochodna MUSI podać cenę”. W C# robi to słowo abstract, a pilnuje tego kompilator.

TEORIA

Klasa, której nie da się utworzyć

Wystarczy jedno słowo przed class:

definicje typów
abstract class Bilet
{
    public string Wlasciciel { get; }

    public Bilet(string wlasciciel)
    {
        Wlasciciel = wlasciciel;
    }
}

class BiletUczniowski : Bilet
{
    public BiletUczniowski(string w) : base(w) { }
}
Program.cs
Bilet a = new BiletUczniowski("Kasia");   // wolno - obiekt klasy pochodnej
Bilet b = new Bilet("Marek");             // BLAD CS0144

CS0144

„Nie można utworzyć wystąpienia klasy abstrakcyjnej Bilet”. To jedyny komunikat, którego chcieliśmy — kasa nie może już wydać „zwykłego biletu”. Zauważ, że zmienna typu Bilet jest nadal całkowicie legalna; zakazane jest tylko new.

To rozróżnienie jest sedno całej lekcji, więc zapiszmy je wprost:

ZapisWolno?Dlaczego
abstract class Bilettakto definicja klasy
Bilet b;takzmienna to tylko okno na obiekt
Bilet[] kasa = new Bilet[10];taktablica przechowuje odwołania, jeszcze nie obiekty
List<Bilet> lista = new();taklista odwołań do klas pochodnych
b = new BiletUczniowski("Kasia");takobiekt klasy pochodnej, nie abstrakcyjnej
b = new Bilet("Marek");nieCS0144 — klasy abstrakcyjnej nie da się utworzyć

Znasz już klasy abstrakcyjne z biblioteki

Klasa Stream jest abstrakcyjna — nie ma „zwykłego strumienia”, są pliki, sieć i pamięć. Tak samo Array z lekcji 24. Nigdy nie napisałeś new Array(), bo nie ma takiej rzeczy: jest tablica czegoś.

TEORIA

Zapowiedź bez treści

Zakaz tworzenia obiektów to połowa rozwiązania. Druga połowa to wymuszenie, żeby każda klasa pochodna podała cenę. Służy do tego składowa abstract — zapowiedź bez treści:

definicja typu
abstract class Bilet
{
    public string Wlasciciel { get; }

    protected Bilet(string wlasciciel)
    {
        Wlasciciel = wlasciciel;
    }

    public abstract decimal Cena { get; }     // BEZ ciala - tylko obietnica
    public abstract string Rodzaj { get; }
}

Zwróć uwagę na trzy rzeczy w tym zapisie:

  • Po Cena { get; } stoi średnik, a nie klamry. Klasa abstrakcyjna nie podaje treści — tylko nazwę, typ i to, że coś takiego musi istnieć.
  • Konstruktor jest protected, nie public. Skoro nikt z zewnątrz nie utworzy obiektu, publiczny konstruktor byłby mylący. Klasy pochodne nadal go wywołają przez base.
  • Nie ma słowa virtual. Składowa abstract jest wirtualna z definicji — dopisanie virtual to błąd CS0503.

Teraz kompilator zaczyna pilnować klas pochodnych:

niepełna klasa pochodna
class BiletUczniowski : Bilet            // BLAD CS0534
{
    public BiletUczniowski(string w) : base(w) { }
    // nie przeslonilem Cena ani Rodzaj
}

CS0534 — to jest cel całej lekcji

„BiletUczniowski nie implementuje dziedziczonego abstrakcyjnego elementu członkowskiego Bilet.Cena”. Program nie skompiluje się, dopóki klasa pochodna nie poda ceny. Porównaj to z lekcją 46: tam zapomniany override dawał tylko ostrzeżenie i cichy błąd w wynikach. Tutaj jest twardy błąd, więc pomyłka nie ma jak dojść do ucznia.

Poprawna klasa pochodna wygląda tak:

definicje typów
class BiletUczniowski : Bilet
{
    public BiletUczniowski(string w) : base(w) { }

    public override decimal Cena => 10m;
    public override string Rodzaj => "uczniowski";
}

class BiletNauczycielski : Bilet
{
    public BiletNauczycielski(string w) : base(w) { }

    public override decimal Cena => 15m;
    public override string Rodzaj => "nauczycielski";
}

class BiletRodzinny : Bilet
{
    public int LiczbaOsob { get; }

    public BiletRodzinny(string w, int liczbaOsob) : base(w)
    {
        LiczbaOsob = liczbaOsob;
    }

    public override decimal Cena => 8m * LiczbaOsob;
    public override string Rodzaj => $"rodzinny ({LiczbaOsob} os.)";
}

override zostaje takie samo jak w lekcji 45 — przesłaniamy składową abstrakcyjną dokładnie tym samym słowem.

Metody abstrakcyjne

To samo dotyczy metod. Zapowiedź bez ciała, zakończona średnikiem:

definicje typów
abstract class Figura
{
    public string Nazwa { get; }

    protected Figura(string nazwa) { Nazwa = nazwa; }

    public abstract double Pole();             // zapowiedz
    public abstract double Obwod();            // zapowiedz
}

class Kwadrat : Figura
{
    private readonly double a;

    public Kwadrat(double a) : base("kwadrat") { this.a = a; }

    public override double Pole() { return a * a; }
    public override double Obwod() { return 4 * a; }
}

Nie ma już żadnego „zwróć zero, bo trzeba coś zwrócić”. Każda figura liczy własne pole, bo inaczej program się nie zbuduje.

TEORIA

Trzy rodzaje składowych w jednej klasie

Częste nieporozumienie: „klasa abstrakcyjna to taka, w której nic nie ma”. Nieprawda. Klasa abstrakcyjna może zawierać wszystko, co zwykła — plus składowe abstrakcyjne.

definicja typu
abstract class Bilet
{
    private static int licznik;              // pole statyczne (lekcja 44)

    public string Wlasciciel { get; }        // wlasciwosc zwykla
    public int Numer { get; }
    protected DateTime data;                 // pole protected (lekcja 45)

    protected Bilet(string wlasciciel)       // konstruktor
    {
        licznik++;
        Numer = licznik;
        Wlasciciel = wlasciciel;
        data = DateTime.Now;
    }

    public abstract decimal Cena { get; }    // abstrakcyjna - MUSI byc przeslonieta

    public virtual string Uwagi => "brak";   // wirtualna - MOZE byc przeslonieta

    public string Naglowek()                 // zwykla - wspolna dla wszystkich
    {
        return $"Bilet nr {Numer} / {data:dd.MM.yyyy}";
    }

    public static int Sprzedanych => licznik;   // wlasciwosc statyczna
}

Trzy rodzaje składowych w jednej klasie, każdy z inną rolą:

SkładowaMa ciało w bazie?Klasa pochodnaKiedy tego użyć
abstractniemusi przesłonić (CS0534)Nie ma sensownej wersji domyślnej. Cena biletu, pole figury.
virtualtakmoże przesłonićJest sensowna wersja domyślna, ale wolno ją zmienić. Uwagi na bilecie.
zwykłataknie może przesłonićZachowanie identyczne dla wszystkich. Nagłówek biletu, numerowanie.
statictakwspólna dla całej hierarchiiLicznik sprzedanych biletów.

Najczęstsze pomyłki w zapisie

  • abstract virtual decimal Cena { get; } — CS0503, abstract już jest wirtualne.
  • public abstract decimal Cena => 0m; — CS0500, składowa abstrakcyjna nie może mieć ciała.
  • abstract w klasie, która nie jest abstract — CS0513. Jedna składowa bez treści czyni całą klasę niepełną.
  • private abstract — CS0621. Skoro klasa pochodna ma to przesłonić, musi to widzieć: zostaje public lub protected.

Klasa abstrakcyjna pochodna od abstrakcyjnej

Klasa pochodna może zostać abstrakcyjna i przekazać obowiązek dalej — przesłania to, co umie, a resztę pozostawia:

definicje typów
abstract class Bilet
{
    public abstract decimal Cena { get; }
    public abstract string Rodzaj { get; }
}

abstract class BiletZnizkowy : Bilet          // tez abstrakcyjna
{
    public override string Rodzaj => "znizkowy";     // to juz wiemy
    public abstract int ProcentZnizki { get; }        // to dopisze klasa nizej
    public override decimal Cena => 20m * (100 - ProcentZnizki) / 100m;
}

class BiletUczniowski : BiletZnizkowy         // zwykla klasa - musi domknac wszystko
{
    public override int ProcentZnizki => 50;
}

BiletZnizkowy przesłoniła Rodzaj i Cena, ale dołożyła własną składową abstrakcyjną. Dopiero BiletUczniowski nie ma już żadnych długów, więc może być klasą zwykłą — i tylko jej obiekt da się utworzyć.

TEORIA

Wspólny szkielet, zmienne szczegóły

Najciekawsze zastosowanie klasy abstrakcyjnej to gotowy szkielet działania: zwykła metoda w klasie bazowej ustala kolejność kroków, a kroki, które różnią się w każdej klasie, są abstrakcyjne.

Wydruk biletu ma zawsze tę samą postać — ramka, nagłówek, dane, cena, ramka. Różni się tylko zawartość:

definicja typu
abstract class Bilet
{
    public string Wlasciciel { get; }

    protected Bilet(string wlasciciel) { Wlasciciel = wlasciciel; }

    public abstract decimal Cena { get; }
    public abstract string Rodzaj { get; }
    public virtual string Uwagi => "brak";

    public void Wydrukuj()                 // szkielet - zwykla metoda, jedna dla wszystkich
    {
        Console.WriteLine("+------------------------------+");
        Console.WriteLine($"| {Rodzaj,-28} |");
        Console.WriteLine($"| {Wlasciciel,-28} |");
        Console.WriteLine($"| do zaplaty: {Cena,10:F2} zl   |");
        Console.WriteLine($"| {Uwagi,-28} |");
        Console.WriteLine("+------------------------------+");
    }
}

Metoda Wydrukuj jest napisana raz i nie jest wirtualna — nikt nie zepsuje formatu biletu. A mimo to każdy bilet drukuje się inaczej, bo Rodzaj, Cena i Uwagi to składowe klasy pochodnej.

Program.cs
Bilet[] kasa =
{
    new BiletUczniowski("Kasia Nowak"),
    new BiletRodzinny("Rodzina Kowalskich", 4)
};

foreach (Bilet b in kasa)
{
    b.Wydrukuj();
}
wynik w konsoli
+------------------------------+
| uczniowski                   |
| Kasia Nowak                  |
| do zaplaty:      10,00 zl   |
| brak                         |
+------------------------------+
+------------------------------+
| rodzinny (4 os.)             |
| Rodzina Kowalskich           |
| do zaplaty:      32,00 zl   |
| wstep dla 4 osob             |
+------------------------------+

Nazwa tego rozwiązania

Taki układ nazywa się metodą szablonową: baza ustala kolejność, klasy pochodne dostarczają szczegóły. Wrócisz do niego w praktyce przy każdym raporcie, każdym eksporcie danych i każdym menu. Zapamiętaj pytanie: „co jest wspólne dla wszystkich, a co zmienne?” — wspólne robisz zwykłą metodą, zmienne abstrakcyjną.

TEORIA

Klasa zwykła, abstrakcyjna czy interfejs

Trzy narzędzia wyglądają podobnie i studenci mieszają je przez lata. Różnica jest prosta, jeśli zadasz właściwe pytanie.

PytanieOdpowiedźNarzędzie
Czy „zwykły X” istnieje w rzeczywistości?tak, np. zwykłe urządzeniezwykła klasa bazowa (lekcja 45)
Czy „zwykły X” istnieje w rzeczywistości?nie, są tylko rodzajeabstract class
Czy klasy mają wspólne pola i kod?takklasa bazowa — zwykła albo abstrakcyjna
Czy klasy mają wspólne pola i kod?nie, tylko wspólną umiejętnośćinterfejs (lekcja 48)
Czy klasa ma już inną klasę bazową?takinterfejs — baza może być tylko jedna

Test jednym zdaniem

Powiedz na głos: „istnieje coś takiego jak zwykły bilet”. Brzmi fałszywie — więc Bilet jest abstrakcyjny. Teraz: „istnieje coś takiego jak zwykłe urządzenie”. Brzmi sensownie — więc Urzadzenie z lekcji 45 może zostać klasą zwykłą.

Trzy sygnały, że klasa powinna być abstrakcyjna

  • Piszesz w metodzie bazowej return 0;, return ""; albo throw new Exception("nie zaimplementowano"), bo nie wiesz, co tam wpisać.
  • W kodzie nie ma i nigdy nie będzie ani jednego new NazwaBazowa(...).
  • Nazwa klasy jest ogólniejsza od wszystkiego, co da się kupić, zobaczyć albo policzyć: Figura, Bilet, Pracownik, Pojazd.
PRZYKŁAD

Kasa turnieju

Kasa turnieju: klasa abstrakcyjna z licznikiem, szkieletem wydruku i dwiema składowymi abstrakcyjnymi, plus trzy rodzaje biletów.

Program.cs — pełny plik
// Program.cs - pelny plik
List<Bilet> kasa = new List<Bilet>
{
    new BiletUczniowski("Kasia Nowak", "3TP"),
    new BiletUczniowski("Marek Lis", "2TP"),
    new BiletNauczycielski("Anna Wrona"),
    new BiletRodzinny("Rodzina Kowalskich", 4)
};

Console.WriteLine("=== wydruk biletow ===");
foreach (Bilet b in kasa)
{
    b.Wydrukuj();
}

decimal suma = 0m;
foreach (Bilet b in kasa)
{
    suma += b.Cena;
}

Console.WriteLine($"Sprzedanych biletow: {Bilet.Sprzedanych}");
Console.WriteLine($"Utarg: {suma:F2} zl");

Console.WriteLine();
Console.WriteLine("=== czego kompilator nie pozwoli ===");
// Bilet zwykly = new Bilet("Nikt");        // CS0144 - klasa abstrakcyjna
// class BiletVip : Bilet { }               // CS0534 - brak Cena i Rodzaj
Console.WriteLine("oba wiersze wyzej sa zakomentowane - odkomentuj i sprawdz");


abstract class Bilet
{
    private static int licznik;

    public int Numer { get; }
    public string Wlasciciel { get; }
    protected DateTime data;

    protected Bilet(string wlasciciel)
    {
        licznik++;
        Numer = licznik;
        Wlasciciel = wlasciciel;
        data = new DateTime(2026, 5, 12);
    }

    public static int Sprzedanych => licznik;

    public abstract decimal Cena { get; }
    public abstract string Rodzaj { get; }

    public virtual string Uwagi => "brak uwag";

    public void Wydrukuj()
    {
        Console.WriteLine("+--------------------------------+");
        Console.WriteLine($"| bilet nr {Numer,-21} |");
        Console.WriteLine($"| {Rodzaj,-30} |");
        Console.WriteLine($"| {Wlasciciel,-30} |");
        Console.WriteLine($"| {data,-30:dd.MM.yyyy} |");
        Console.WriteLine($"| do zaplaty: {Cena,8:F2} zl        |");
        Console.WriteLine($"| {Uwagi,-30} |");
        Console.WriteLine("+--------------------------------+");
    }

    public override string ToString()
    {
        return $"#{Numer} {Rodzaj}: {Wlasciciel}, {Cena:F2} zl";
    }
}

class BiletUczniowski : Bilet
{
    public string Klasa { get; }

    public BiletUczniowski(string wlasciciel, string klasa) : base(wlasciciel)
    {
        Klasa = klasa;
    }

    public override decimal Cena => 10m;
    public override string Rodzaj => "uczniowski";
    public override string Uwagi => $"klasa {Klasa}, wymagana legitymacja";
}

class BiletNauczycielski : Bilet
{
    public BiletNauczycielski(string wlasciciel) : base(wlasciciel) { }

    public override decimal Cena => 15m;
    public override string Rodzaj => "nauczycielski";
}

class BiletRodzinny : Bilet
{
    public int LiczbaOsob { get; }

    public BiletRodzinny(string wlasciciel, int liczbaOsob) : base(wlasciciel)
    {
        LiczbaOsob = liczbaOsob;
    }

    public override decimal Cena => 8m * LiczbaOsob;
    public override string Rodzaj => $"rodzinny ({LiczbaOsob} os.)";
    public override string Uwagi => $"wstep dla {LiczbaOsob} osob";
}
wynik w konsoli
=== wydruk biletow ===
+--------------------------------+
| bilet nr 1                     |
| uczniowski                     |
| Kasia Nowak                    |
| 12.05.2026                     |
| do zaplaty:    10,00 zl        |
| klasa 3TP, wymagana legitymacja |
+--------------------------------+
+--------------------------------+
| bilet nr 2                     |
| uczniowski                     |
| Marek Lis                      |
| 12.05.2026                     |
| do zaplaty:    10,00 zl        |
| klasa 2TP, wymagana legitymacja |
+--------------------------------+
+--------------------------------+
| bilet nr 3                     |
| nauczycielski                  |
| Anna Wrona                     |
| 12.05.2026                     |
| do zaplaty:    15,00 zl        |
| brak uwag                      |
+--------------------------------+
+--------------------------------+
| bilet nr 4                     |
| rodzinny (4 os.)               |
| Rodzina Kowalskich             |
| 12.05.2026                     |
| do zaplaty:    32,00 zl        |
| wstep dla 4 osob               |
+--------------------------------+
Sprzedanych biletow: 4
Utarg: 67,00 zl

=== czego kompilator nie pozwoli ===
oba wiersze wyzej sa zakomentowane - odkomentuj i sprawdz

Co dzieje się po kolei

  • Lista jest typu List<Bilet>, choć Bilet jest abstrakcyjny. To legalne — lista przechowuje odwołania do obiektów klas pochodnych.
  • Numer biletu nadaje konstruktor klasy abstrakcyjnej przez pole statyczne. Kod numerowania jest napisany raz i nie da się go pominąć, bo każdy konstruktor pochodny musi wywołać base.
  • Wydrukuj nie jest wirtualna, więc wszystkie bilety mają identyczną ramkę. Wnętrze różni się, bo Rodzaj, Cena i Uwagi pochodzą z klasy pochodnej.
  • BiletNauczycielski nie przesłania Uwagi — bierze wersję virtual z klasy bazowej, czyli „brak uwag”. Gdyby Uwagi była abstract, ta klasa by się nie skompilowała.
  • Cena biletu rodzinnego liczy się z liczby osób: 8 × 4 = 32 zł. Utarg to 10 + 10 + 15 + 32 = 67 zł.
  • Ostatni wiersz przypomina o dwóch zakazach — obu pilnuje kompilator, a nie czujność programisty.

Sprawdź trzy rzeczy

  1. Odkomentuj new Bilet("Nikt"). Dostaniesz CS0144 — dokładnie ten błąd, którego nie było w wersji z lekcji 46, gdzie taki bilet dawał się utworzyć za 0 zł.
  2. Dopisz class BiletVip : Bilet { } bez żadnej składowej. Zobaczysz dwa razy CS0534 i kompilator wskaże, czego brakuje.
  3. Zmień public virtual string Uwagi na public abstract string Uwagi { get; }. Teraz BiletNauczycielski przestanie się kompilować — to pokazuje różnicę między „może” a „musi”.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
abstract classklasa, której nie da się utworzyćZmienne, tablice i listy tego typu są legalne. new daje CS0144.
abstract przy metodziezapowiedź bez ciałaKończy się średnikiem. Klasa pochodna musi ją przesłonić.
abstract przy właściwościzapowiedź akcesorówpublic abstract decimal Cena { get; }
overridepodanie treści zapowiedziTo samo słowo co przy virtual z lekcji 45.
protected konstruktorkonstruktor tylko dla klas pochodnychNaturalny wybór w klasie abstrakcyjnej.
klasa pochodna abstractprzekazanie obowiązku dalejMoże przesłonić część składowych i dodać własne abstrakcyjne.
zwykła metoda w klasie abstrakcyjnejwspólne zachowanieMetoda szablonowa: ustala kolejność, woła składowe abstrakcyjne.
static w klasie abstrakcyjnejskładowa całej hierarchiiNp. licznik utworzonych obiektów.
CS0144próba new na klasie abstrakcyjnejUtwórz obiekt klasy pochodnej.
CS0534klasa pochodna nie przesłoniła składowej abstrakcyjnejDopisz override.
CS0500 / CS0503ciało przy abstract / abstract virtualSkładowa abstrakcyjna nie ma ciała i jest wirtualna z definicji.
CS0513abstract w klasie, która nie jest abstrakcyjnaDopisz abstract przed class.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
new Bilet(...) na klasie abstrakcyjnejCS0144. Zwykle znak, że chciałeś utworzyć konkretny rodzaj, a nie kategorię.
Klasa pochodna bez override składowej abstrakcyjnejCS0534. Kompilator wypisze nazwę każdej brakującej składowej — przeczytaj listę.
public abstract decimal Cena => 0m;CS0500. Jeśli chcesz wersji domyślnej, to ma być virtual, nie abstract.
public abstract virtual void Rysuj();CS0503. abstract jest już wirtualne.
private abstractCS0621. Klasa pochodna musi widzieć to, co ma przesłonić.
Jedna składowa abstract w zwykłej klasieCS0513. Cała klasa musi dostać abstract.
abstract i sealed na jednej klasieCS0418. Sprzeczność: nie da się utworzyć i nie da się dziedziczyć, więc klasa byłaby bezużyteczna.
virtual tam, gdzie miało być abstractKompiluje się, ale wraca problem z początku lekcji: zapomniane przesłonięcie daje cichą wartość domyślną.
Wszystko abstract, nic wspólnego w bazieJeśli w klasie nie ma ani pola, ani jednej zwykłej metody, to prawdopodobnie powinien to być interfejs (lekcja 48).
Składowa abstrakcyjna wołana w konstruktorze bazowymWykona się wersja pochodna, zanim jej pola dostaną wartości. Ten sam problem co z virtual.
ZADANIA

Zadania

ZAD 1Figura, której nie da się utworzyć★☆☆

Napisz klasę abstrakcyjną Figura z właściwością Nazwa ustawianą w chronionym konstruktorze oraz dwiema metodami abstrakcyjnymi: double Pole() i double Obwod(). Napisz klasy Kwadrat i Kolo, które je przesłaniają. W programie utwórz po jednym obiekcie każdej klasy i wypisz nazwę, pole i obwód z dokładnością do dwóch miejsc. Następnie dopisz wiersz Figura f = new Figura("cos");, spróbuj skompilować i zapisz w komentarzu numer błędu, a potem zakomentuj ten wiersz.

ZAD 2Kompilator pilnuje kompletności★☆☆

Do zadania 1 dopisz klasę Trojkat dziedziczącą po Figura, ale na razie bez żadnej metody. Spróbuj skompilować, zapisz w komentarzu numer błędu i nazwy składowych, które kompilator wymienił. Potem uzupełnij obie metody (pole ze wzoru na podstawę i wysokość, obwód z trzech boków) i sprawdź, że program się buduje. Utwórz tablicę Figura[] z trzema figurami i wypisz je jedną pętlą.

ZAD 3abstract kontra virtual★★☆

Do klasy Figura z zadania 1 dodaj dwie składowe: właściwość abstrakcyjną string Symbol { get; } oraz właściwość wirtualną string Opis zwracającą tekst „figura płaska”. Zwróć uwagę, że Symbol jest abstrakcyjna, więc każda klasa pochodna musi ją przesłonić — wybór dotyczy wyłącznie Opis. W klasie Kwadrat przesłoń obie składowe, a w klasach Kolo i Trojkat — tylko Symbol. Następnie spróbuj usunąć przesłonięcie Symbol z klasy Kolo i zapisz numer błędu. Uruchom program i wypisz dla każdej figury symbol oraz opis. W komentarzu wyjaśnij, dlaczego Kolo kompiluje się bez przesłonięcia Opis, a nie skompilowałoby się bez przesłonięcia Symbol.

ZAD 4Metoda szablonowa★★☆

Napisz klasę abstrakcyjną Raport z właściwością Tytul, abstrakcyjną metodą string[] Wiersze() oraz zwykłą metodą void Wypisz(), która drukuje ramkę, tytuł wyśrodkowany, wszystkie wiersze z Wiersze(), na końcu liczbę wierszy i ramkę zamykającą. Napisz dwie klasy pochodne: RaportOcen (wiersze w formacie przedmiot: ocena) i RaportObecnosci (wiersze w formacie data — liczba nieobecnych). W programie utwórz tablicę Raport[] i wypisz oba raporty jedną pętlą. Sprawdź, że format ramki jest napisany w programie tylko raz.

ZAD 5Trzypoziomowa hierarchia z klasą pośrednią★★☆

Napisz klasę abstrakcyjną Pracownik z Imie, Nazwisko, abstrakcyjną właściwością decimal Pensja { get; } i abstrakcyjną właściwością string Stanowisko { get; }. Napisz abstrakcyjną klasę pośrednią PracownikEtatowy, która przesłania Pensja wzorem „stawka miesięczna razy etat” i dodaje dwie nowe składowe abstrakcyjne: decimal StawkaMiesieczna { get; } oraz double Etat { get; }. Napisz dwie zwykłe klasy pochodne od PracownikEtatowy z konkretnymi stawkami i etatami oraz jedną klasę PracownikZlecenie dziedziczącą wprost po Pracownik, u której pensja to stawka godzinowa razy liczba godzin. Utwórz listę pracowników, wypisz ją w kolumnach i policz sumę pensji. W komentarzu napisz, których klas nie da się utworzyć i dlaczego.

ZAD 6Konta bankowe — pełny program★★★

Napisz klasę abstrakcyjną Konto z: właściwością Numer nadawaną automatycznie przez licznik statyczny, właściwością Wlasciciel, polem protected decimal saldo, właściwością tylko do odczytu Saldo, abstrakcyjną właściwością string Typ { get; }, abstrakcyjną metodą decimal MiesieczneKoszty(), wirtualną metodą bool CzyMoznaWyplacic(decimal kwota) zwracającą true, gdy saldo wystarcza, oraz zwykłymi metodami Wplac(decimal) i Wyplac(decimal). Metoda Wyplac ma być szkieletem: sprawdza kwotę większą od zera, pyta CzyMoznaWyplacic, a gdy wolno — zmniejsza saldo i wypisuje potwierdzenie; gdy nie wolno — wypisuje odmowę z powodem. Napisz trzy klasy pochodne: KontoOsobiste (koszty 6 zł), KontoOszczednosciowe (koszty 0 zł, ale CzyMoznaWyplacic pozwala tylko wtedy, gdy po wypłacie zostanie co najmniej 200 zł) i KontoFirmowe (koszty 25 zł, wypłata dozwolona do limitu debetu 1000 zł poniżej zera). W programie utwórz listę co najmniej czterech kont, wykonaj na każdym po dwie wypłaty — jedną możliwą i jedną nie — a na końcu wypisz tabelkę z numerem, typem, właścicielem, saldem i miesięcznymi kosztami oraz sumę wszystkich kosztów. W komentarzu odpowiedz na dwa pytania: dlaczego Wyplac nie jest wirtualna, oraz co trzeba by zmienić w programie, żeby dodać czwarty typ konta.

PODSUMOWANIE

Co trzeba zapamiętać

  • abstract class to kategoria, nie rzecz — nie da się jej utworzyć przez new (CS0144).
  • Zmienne, tablice i listy typu abstrakcyjnego są w pełni legalne; zakazane jest tylko tworzenie obiektu.
  • Składowa abstract to zapowiedź bez ciała, zakończona średnikiem.
  • Klasa pochodna musi przesłonić każdą składową abstrakcyjną — inaczej CS0534. To błąd, nie ostrzeżenie.
  • abstract znaczy „musisz”, virtual znaczy „możesz”. To cała różnica między nimi.
  • Składowa abstract jest wirtualna z definicji, więc abstract virtual to błąd.
  • Klasa abstrakcyjna może mieć pola, konstruktor, składowe statyczne i zwykłe metody — nie jest pustą skorupą.
  • Konstruktor klasy abstrakcyjnej wypada oznaczyć protected, bo służy wyłącznie klasom pochodnym.
  • Klasa pochodna też może być abstrakcyjna i przekazać obowiązek jeszcze niżej.
  • Metoda szablonowa: wspólne kroki w zwykłej metodzie bazowej, zmienne szczegóły w składowych abstrakcyjnych.
  • Test decyzyjny: jeśli „zwykły X” nie istnieje w rzeczywistości, klasa X ma być abstrakcyjna.

Dokumentacja: Microsoft Learn — temat tej lekcji.

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