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.

CEL LEKCJI

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.

TEORIA

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.

Fragment C#
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.

Fragment C#
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.

Fragment C#
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.

Fragment C#
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”.

PRZYKŁAD

Przewidź → uruchom → zmień

Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program uruchamiaj w oddzielnym projekcie konsolowym.

Program.cs
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());
    }
}
Wynik przykładu
Uczen: 10 zl
Gosc: 16 zl
TEORIA

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

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.

ZAD 1Ćwiczenie 1★☆☆

Zaprojektuj abstrakcyjny Ksztalt z metodą ObliczPole i dwie figury. Wymiary mają być dodatnie; określ przypadki testowe.

ZAD 2Ćwiczenie 2★☆☆

W eksperymencie usuń override jednej klasy konkretnej. Zapisz, co zgłasza kompilator i dlaczego.

ZAD 3Ćwiczenie 3★★☆

Dodaj abstrakcyjną właściwość Kategoria do własnego przykładu i dostarcz dwa różne odczyty.

ZAD 4Ćwiczenie 4★★☆

Porównaj virtual ze zwracaniem 0 oraz abstract bez implementacji. Wskaż, kiedy zero jest prawidłową wartością, a kiedy maskuje brak kodu.

PODSUMOWANIE

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.