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.
Czego się dziś nauczysz
- Rozpoznasz sytuację, w której klasa bazowa nie ma sensownej wartości domyślnej.
- Zadeklarujesz
abstract classi wyjaśnisz, dlaczego zmienna tego typu jest legalna, anewnie. - Napiszesz metodę i właściwość abstrakcyjną oraz przesłonisz je w klasie pochodnej.
- Odróżnisz
abstractodvirtualjednym 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.
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:
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ę:
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.
Klasa, której nie da się utworzyć
Wystarczy jedno słowo przed class:
abstract class Bilet
{
public string Wlasciciel { get; }
public Bilet(string wlasciciel)
{
Wlasciciel = wlasciciel;
}
}
class BiletUczniowski : Bilet
{
public BiletUczniowski(string w) : base(w) { }
}
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:
| Zapis | Wolno? | Dlaczego |
|---|---|---|
abstract class Bilet | tak | to definicja klasy |
Bilet b; | tak | zmienna to tylko okno na obiekt |
Bilet[] kasa = new Bilet[10]; | tak | tablica przechowuje odwołania, jeszcze nie obiekty |
List<Bilet> lista = new(); | tak | lista odwołań do klas pochodnych |
b = new BiletUczniowski("Kasia"); | tak | obiekt klasy pochodnej, nie abstrakcyjnej |
b = new Bilet("Marek"); | nie | CS0144 — 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ś.
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:
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, niepublic. Skoro nikt z zewnątrz nie utworzy obiektu, publiczny konstruktor byłby mylący. Klasy pochodne nadal go wywołają przezbase. - Nie ma słowa
virtual. Składowaabstractjest wirtualna z definicji — dopisanievirtualto błąd CS0503.
Teraz kompilator zaczyna pilnować klas pochodnych:
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:
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:
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.
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.
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ładowa | Ma ciało w bazie? | Klasa pochodna | Kiedy tego użyć |
|---|---|---|---|
abstract | nie | musi przesłonić (CS0534) | Nie ma sensownej wersji domyślnej. Cena biletu, pole figury. |
virtual | tak | może przesłonić | Jest sensowna wersja domyślna, ale wolno ją zmienić. Uwagi na bilecie. |
| zwykła | tak | nie może przesłonić | Zachowanie identyczne dla wszystkich. Nagłówek biletu, numerowanie. |
static | tak | wspólna dla całej hierarchii | Licznik sprzedanych biletów. |
Najczęstsze pomyłki w zapisie
abstract virtual decimal Cena { get; }— CS0503,abstractjuż jest wirtualne.public abstract decimal Cena => 0m;— CS0500, składowa abstrakcyjna nie może mieć ciała.abstractw klasie, która nie jestabstract— 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ć: zostajepubliclubprotected.
Klasa abstrakcyjna pochodna od abstrakcyjnej
Klasa pochodna może zostać abstrakcyjna i przekazać obowiązek dalej — przesłania to, co umie, a resztę pozostawia:
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ć.
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ść:
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.
Bilet[] kasa =
{
new BiletUczniowski("Kasia Nowak"),
new BiletRodzinny("Rodzina Kowalskich", 4)
};
foreach (Bilet b in kasa)
{
b.Wydrukuj();
}
+------------------------------+
| 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ą.
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.
| Pytanie | Odpowiedź | Narzędzie |
|---|---|---|
| Czy „zwykły X” istnieje w rzeczywistości? | tak, np. zwykłe urządzenie | zwykła klasa bazowa (lekcja 45) |
| Czy „zwykły X” istnieje w rzeczywistości? | nie, są tylko rodzaje | abstract class |
| Czy klasy mają wspólne pola i kod? | tak | klasa 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ą? | tak | interfejs — 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 "";albothrow 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.
Kasa turnieju
Kasa turnieju: klasa abstrakcyjna z licznikiem, szkieletem wydruku i dwiema składowymi abstrakcyjnymi, plus trzy rodzaje biletów.
// 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";
}
=== 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ćBiletjest 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. Wydrukujnie jest wirtualna, więc wszystkie bilety mają identyczną ramkę. Wnętrze różni się, boRodzaj,CenaiUwagipochodzą z klasy pochodnej.BiletNauczycielskinie przesłaniaUwagi— bierze wersjęvirtualz klasy bazowej, czyli „brak uwag”. GdybyUwagibyłaabstract, 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
- 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ł. - Dopisz
class BiletVip : Bilet { }bez żadnej składowej. Zobaczysz dwa razy CS0534 i kompilator wskaże, czego brakuje. - Zmień
public virtual string Uwaginapublic abstract string Uwagi { get; }. TerazBiletNauczycielskiprzestanie się kompilować — to pokazuje różnicę między „może” a „musi”.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
abstract class | klasa, której nie da się utworzyć | Zmienne, tablice i listy tego typu są legalne. new daje CS0144. |
abstract przy metodzie | zapowiedź bez ciała | Kończy się średnikiem. Klasa pochodna musi ją przesłonić. |
abstract przy właściwości | zapowiedź akcesorów | public abstract decimal Cena { get; } |
override | podanie treści zapowiedzi | To samo słowo co przy virtual z lekcji 45. |
protected konstruktor | konstruktor tylko dla klas pochodnych | Naturalny wybór w klasie abstrakcyjnej. |
klasa pochodna abstract | przekazanie obowiązku dalej | Może przesłonić część składowych i dodać własne abstrakcyjne. |
| zwykła metoda w klasie abstrakcyjnej | wspólne zachowanie | Metoda szablonowa: ustala kolejność, woła składowe abstrakcyjne. |
static w klasie abstrakcyjnej | składowa całej hierarchii | Np. licznik utworzonych obiektów. |
| CS0144 | próba new na klasie abstrakcyjnej | Utwórz obiekt klasy pochodnej. |
| CS0534 | klasa pochodna nie przesłoniła składowej abstrakcyjnej | Dopisz override. |
| CS0500 / CS0503 | ciało przy abstract / abstract virtual | Składowa abstrakcyjna nie ma ciała i jest wirtualna z definicji. |
| CS0513 | abstract w klasie, która nie jest abstrakcyjna | Dopisz abstract przed class. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
new Bilet(...) na klasie abstrakcyjnej | CS0144. Zwykle znak, że chciałeś utworzyć konkretny rodzaj, a nie kategorię. |
Klasa pochodna bez override składowej abstrakcyjnej | CS0534. 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 abstract | CS0621. Klasa pochodna musi widzieć to, co ma przesłonić. |
Jedna składowa abstract w zwykłej klasie | CS0513. Cała klasa musi dostać abstract. |
abstract i sealed na jednej klasie | CS0418. Sprzeczność: nie da się utworzyć i nie da się dziedziczyć, więc klasa byłaby bezużyteczna. |
virtual tam, gdzie miało być abstract | Kompiluje się, ale wraca problem z początku lekcji: zapomniane przesłonięcie daje cichą wartość domyślną. |
Wszystko abstract, nic wspólnego w bazie | Jeś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 bazowym | Wykona się wersja pochodna, zanim jej pola dostaną wartości. Ten sam problem co z virtual. |
Zadania
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.
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ą.
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.
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.
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.
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.
Co trzeba zapamiętać
abstract classto kategoria, nie rzecz — nie da się jej utworzyć przeznew(CS0144).- Zmienne, tablice i listy typu abstrakcyjnego są w pełni legalne; zakazane jest tylko tworzenie obiektu.
- Składowa
abstractto 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.
abstractznaczy „musisz”,virtualznaczy „możesz”. To cała różnica między nimi.- Składowa
abstractjest wirtualna z definicji, więcabstract virtualto 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
Xma być abstrakcyjna.
Dokumentacja: Microsoft Learn — temat tej lekcji.