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
Po lekcji wyjaśnisz poznany mechanizm, prześledzisz działanie programu i zastosujesz go w samodzielnym zadaniu. Pracuj po kolei według spisu OOP. Potrzebujesz podstaw zmiennych, warunków, metod i wcześniejszych tematów tego modułu.
Zaplanuj 1–2 godziny lekcyjne, a zadania rozszerzające wykonaj w osobnym laboratorium. Używamy .NET 8 lub nowszego. Nie łącz kilku kompletnych przykładów w jednym Program.cs.
Wyjaśnienie krok po kroku
Dlaczego zwykła baza czasem nie wystarcza?
Virtual daje wersję domyślną. Jeśli każda konkretna klasa musi samodzielnie określić zachowanie, łatwo zapomnieć o override i nie zauważyć problemu. Metoda abstract nie ma ciała i wyraża obowiązek implementacji. Kompilator sprawdzi brakujący element, ale nie sprawdzi, czy wybrany wzór jest merytorycznie dobry.
Abstract class
Nie można wykonać new dla klasy abstrakcyjnej. Można zadeklarować zmienną jej typu i przypisać obiekt klasy konkretnej. Klasa abstrakcyjna może mieć pola, konstruktor, właściwości oraz metody zwykłe, virtual i abstract. Nie musi zawierać żadnej metody abstract.
Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.
abstract class Bilet
{
public abstract int Cena();
}
// new Bilet() jest niedozwolone.
Abstract class nie pozwala tworzyć bezpośrednich instancji. Abstrakcyjna metoda kończy się średnikiem, nie ma ciała.
Ciało kontra średnik
public abstract int Cena(); kończy się średnikiem, bez bloku. W klasie konkretnej
implementujemy ją przez override. Jeśli pośrednia klasa też jest abstrakcyjna, może pozostawić
obowiązek dalszej klasie. Pierwsza konkretna klasa musi zapewnić implementację wszystkich nadal
abstrakcyjnych składowych.
Tak wygląda to w kodzie — Definicja klasy pochodnej; bazowy Bilet ma abstract Cena. To fragment ilustracyjny, nie cały Program.cs.
class BiletUcznia : Bilet
{
public override int Cena() { return 10; }
}
Konkretna klasa wypełnia obowiązek bazy: dostarcza ciało Cena. Dziesięć to umowna cena w ćwiczeniu.
Konstruktor bazy nadal pracuje
Nie tworzymy osobnego obiektu abstrakcyjnej bazy, ale jej konstruktor przygotowuje bazową część
obiektu pochodnego. : base(nazwa) przekazuje argument. Protected pozwala używać
konstruktora w klasach pochodnych. Nazwa zostanie ustawiona przed wykonaniem ciała konstruktora
pochodnego.
Tak wygląda to w kodzie — Definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
abstract class Bilet
{
public string Nazwa { get; }
protected Bilet(string nazwa) { Nazwa = nazwa; }
}
class BiletUcznia : Bilet
{
public BiletUcznia() : base("Uczen") { }
}
Base przekazuje tekst do konstruktora bazy. Nazwa jest częścią tego samego obiektu BiletUcznia.
Właściwości też mogą być abstrakcyjne
Zapis public abstract string Kategoria { get; } wymaga dostarczenia odczytu. Klasa
pochodna może obliczać wartość lub ją przechowywać. Dobieraj postać do znaczenia: krótka cecha
pasuje do właściwości, działanie do metody.
Tak wygląda to w kodzie — Definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
abstract class Bilet
{
public abstract string Kategoria { get; }
}
class BiletUcznia : Bilet
{
public override string Kategoria => "Uczniowski";
}
Baza wymaga odczytu kategorii. Klasa pochodna daje wartość przez obliczany getter, bez własnego pola.
Abstract czy interface?
Abstrakcyjna baza jest użyteczna, gdy model wymaga wspólnego stanu i częściowej implementacji. Interfejs określa zdolność potrzebną klientowi, także dla niepowiązanych klas. Klasa może mieć jedną bezpośrednią bazę, lecz wiele interfejsów. Współczesne interfejsy mogą mieć pewne implementacje; nie opieraj wyboru na nieprawdziwym haśle „interfejs nigdy nie ma kodu”.
Przewidź → uruchom → zmień
Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program
uruchamiaj w oddzielnym projekcie konsolowym.
using System;
abstract class Bilet
{
public string Nazwa { get; }
protected Bilet(string nazwa) { Nazwa = nazwa; }
public abstract int Cena();
public string Opis() { return $"{Nazwa}: {Cena()} zl"; }
}
class BiletUcznia : Bilet
{
public BiletUcznia() : base("Uczen") { }
public override int Cena() { return 10; }
}
class BiletGoscia : Bilet
{
public BiletGoscia() : base("Gosc") { }
public override int Cena() { return 16; }
}
class Program
{
static void Main()
{
Bilet[] bilety = { new BiletUcznia(), new BiletGoscia() };
foreach (Bilet bilet in bilety) Console.WriteLine(bilet.Opis());
}
}
Uczen: 10 zl
Gosc: 16 zl
Co dokładnie robi kod?
Ceny są umownymi danymi ćwiczenia. Opis jest jedną zwykłą metodą w bazie, ale wywołuje polimorficzne Cena. To połączenie wspólnego algorytmu i konkretnego szczegółu. Base wywołuje konstruktor bazowy, a nie tworzy dodatkowego biletu. Cena i Opis są naszym kodem. Console.WriteLine(string) wypisuje opis i kończy wiersz. Nie używamy tu metod obliczających podatki ani rzeczywistych cenników.
Zmień jedną daną i zapisz przewidywany wynik przed uruchomieniem. Następnie porównaj obserwację z przewidywaniem i wskaż instrukcję odpowiedzialną za różnicę.
Zadania
Nie ma tu gotowych rozwiązań. Oddaj kod, własne przypadki testowe oraz krótkie wyjaśnienie decyzji. Pierwsze zadanie jest podstawowe; ostatnie pozwalają pogłębić temat.
Zaprojektuj abstrakcyjny Ksztalt z metodą ObliczPole i dwie figury. Wymiary mają być dodatnie; określ przypadki testowe.
W eksperymencie usuń override jednej klasy konkretnej. Zapisz, co zgłasza kompilator i dlaczego.
Dodaj abstrakcyjną właściwość Kategoria do własnego przykładu i dostarcz dwa różne odczyty.
Porównaj virtual ze zwracaniem 0 oraz abstract bez implementacji. Wskaż, kiedy zero jest prawidłową wartością, a kiedy maskuje brak kodu.
Sprawdź, czy rozumiesz
- Czy abstrakcyjna klasa może mieć konstruktor?
- Czy musi zawierać metodę abstract?
- Czy kompilator sprawdza poprawność wzoru w override?
Dokumentacja: Microsoft Learn — Klasy abstrakcyjne.