Menu

Klasa abstrakcyjna w C#: metody abstrakcyjne, przykłady i porównanie z interfejsem

Czym jest klasa abstrakcyjna w C#: klasa bazowa, której nie można zinstancjonować i która może deklarować abstrakcyjne składowe, obowiązkowe do zaimplementowania w klasach pochodnych. Omawia metody i właściwości abstrakcyjne, konstruktory, wzorzec template method oraz to, kiedy lepiej wybrać interfejs.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Klasa abstrakcyjna to klasa, która istnieje tylko po to, żeby po niej dziedziczyć. Nie można utworzyć jej instancji, a może deklarować składowe abstrakcyjne: metody i właściwości bez ciała, które musi zaimplementować każda konkretna klasa pochodna. Poza tym to zwykła klasa: może mieć pola, konstruktory i w pełni zaimplementowane metody, które klasy pochodne dziedziczą.

Deklarowanie klasy abstrakcyjnej

Oznacz klasę jako abstract, a każdą składową, którą muszą dostarczyć klasy pochodne, również oznacz abstract i zakończ średnikiem zamiast ciała:

Wynik:

Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00

Describe jest napisana raz, w klasie bazowej, i wywołuje Area oraz Perimeter, których w Shape w ogóle nie ma. Na tym polega sens składowej abstrakcyjnej: klasa bazowa może na niej polegać, a kompilator gwarantuje, że każdy konkretny kształt ją dostarcza. Klasa pochodna, w której jej zabraknie, się nie skompiluje:

class Triangle : Shape
{
    public Triangle() : base("Triangle") { }
    public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'

Rozwiązaniem jest zaimplementowanie Perimeter albo oznaczenie samego Triangle jako abstract, jeśli to pośrednia klasa bazowa dla kolejnych klas.

Zasady dotyczące składowych abstrakcyjnych

  • Składowa abstrakcyjna nie ma ciała i jest niejawnie wirtualna: klasy pochodne implementują ją przez override.
  • Tylko klasy abstrakcyjne mogą zawierać składowe abstrakcyjne. Umieszczenie takiej składowej w zwykłej klasie to błąd kompilacji.
  • Składowe abstrakcyjne w klasie nie mogą być private (nikt nie mógłby ich nadpisać) ani static.
  • Abstrakcyjne mogą być metody, właściwości, indeksery i zdarzenia. Pola i konstruktory nie.
  • Klasa abstrakcyjna nie musi zawierać żadnej składowej abstrakcyjnej. Samo oznaczenie klasy jako abstract blokuje tworzenie instancji, co przydaje się w klasie bazowej, która ma sens tylko przez swoje podklasy.

Właściwość abstrakcyjna określa, które akcesory musi dostarczyć klasa pochodna:

abstract class Plan
{
    public abstract decimal MonthlyPrice { get; }
    public abstract string Name { get; }
}

class ProPlan : Plan
{
    public override decimal MonthlyPrice => 12.99m;
    public override string Name => "Pro";
}

Konstruktory w klasach abstrakcyjnych

Klasa abstrakcyjna może mieć konstruktory. Nie da się ich wywołać przez new, ale każdy konstruktor pochodny uruchamia jeden z nich przez : base(...), więc to w nich inicjalizuje się i waliduje wspólny stan. Deklaruj je jako protected, co mówi dokładnie to: korzystać z nich mogą tylko klasy pochodne.

Wzorzec template method

Najczęstszy powód pisania klasy abstrakcyjnej to ustalenie kolejności kroków algorytmu w klasie bazowej i pozostawienie klasom pochodnym wypełnienia poszczególnych kroków. Metoda bazowa, która wykonuje kroki, nie jest wirtualna, więc żadna podklasa nie może zmienić ich kolejności ani któregoś pominąć.

Wynik:

item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20

| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |

Total: 26.20

Każdy wiersz to nazwana krotka, a wynik jest budowany za pomocą StringBuilder. Zwróć uwagę na połączenie: Header i Row są abstract, bo każdy format musi je określić, a Footer jest virtual z pustą wartością domyślną, bo większość formatów nic tam nie potrzebuje. Kroki są protected, więc kod z zewnątrz może wywołać tylko Export. Klasy frameworka korzystają z tego samego pomysłu: Stream i TextWriter to abstrakcyjne klasy bazowe, których liczne metody pomocnicze opierają się na kilku składowych dostarczanych przez klasę pochodną (Read i Write dla strumienia, Write(char) dla obiektu zapisującego tekst).

Klasa abstrakcyjna a interfejs

Oba pozwalają pisać kod dla typu bez znajomości konkretnej klasy. Różnią się tym, co mogą zawierać, i tym, ile ich może mieć jedna klasa:

Klasa abstrakcyjnaInterfejs
Klasa może dziedziczyć lub implementowaćjednąwiele
Pola instancji (stan)taknie
Konstruktorytaknie
Metody z ciałemtaktak, od C# 8 (domyślne metody interfejsu)
Składowe statycznetaktak, od C# 8; statyczne składowe abstrakcyjne od C# 11
Modyfikatory dostępu składowychdowolnedomyślnie public; inne dozwolone od C# 8
Mogą go używać strukturynietak
Dodanie składowej późniejbezpieczne, jeśli nie jest abstrakcyjnapsuje implementacje, chyba że ma domyślne ciało

Domyślne metody interfejsu (C# 8) zmniejszyły tę różnicę, ale nie zmieniły jej sedna:

public interface ILogger
{
    void Write(string message);
    void Error(string message) => Write("ERROR: " + message);   // default implementation: C# 8, .NET Core 3.0+
}

Metoda domyślna nadal nie ma dostępu do pól instancji, bo interfejs ich nie ma; może jedynie wywoływać inne składowe interfejsu.

Praktyczny sposób wyboru:

  • Interfejs, gdy opisujesz możliwość, którą mogą mieć niezwiązane ze sobą typy (IComparable<T>, IDisposable, IShippingProvider), gdy mają w tym uczestniczyć struktury albo gdy klasa potrzebuje kilku takich ról. Z tego powodu większość kodu z wstrzykiwaniem zależności pisze się pod interfejsy.
  • Klasa abstrakcyjna, gdy typy pochodne naprawdę są wariantami jednej rzeczy i współdzielą stan lub stały algorytm, jak eksportery powyżej.
  • Oba naraz to częsty układ: interfejs, od którego zależy kod wywołujący, i abstrakcyjna klasa bazowa, która go implementuje i oszczędza implementującym przepisywania wspólnych części. Strona o interfejsach szczegółowo omawia stronę interfejsu.

Częste błędy

  • Próba utworzenia instancji klasy bazowej. new Shape(...) to CS0144. Utwórz instancję klasy pochodnej.
  • Brak override w implementacji. Napisanie public double Area() w klasie pochodnej nie implementuje składowej abstrakcyjnej; kompilator zgłasza CS0534 z powodu brakującej implementacji oraz ostrzeżenie, że nowa metoda ukrywa odziedziczoną.
  • Publiczny konstruktor w klasie abstrakcyjnej. Nieszkodliwy, ale mylący. Użyj protected.
  • Klasy abstrakcyjne bez wspólnego kodu. Jeśli klasa bazowa ma tylko składowe abstrakcyjne i żadnego stanu, interfejs mówi to samo i nie zajmuje jedynego miejsca na klasę bazową.
  • Wywoływanie składowych abstrakcyjnych z konstruktora bazowego. Implementacja pochodna uruchamia się przed ciałem konstruktora pochodnego, więc pola przypisywane w tym ciele nadal mają wartość 0 lub null.

Najczęściej zadawane pytania

Czym jest klasa abstrakcyjna w C#?

Klasa oznaczona jako abstract służy wyłącznie jako klasa bazowa: nie można utworzyć jej instancji przez new (błąd CS0144). Może zawierać zwykłe pola, konstruktory i metody, a do tego składowe abstract, które nie mają ciała i które musi zaimplementować każda nieabstrakcyjna klasa pochodna.

Czym różni się klasa abstrakcyjna od interfejsu w C#?

Klasa może dziedziczyć tylko po jednej klasie abstrakcyjnej, ale implementować wiele interfejsów. Klasa abstrakcyjna może przechowywać stan (pola instancji), mieć konstruktory i składowe o dowolnym poziomie dostępu, więc to naturalne miejsce na wspólną implementację. Interfejs opisuje możliwość bez stanu instancji; od C# 8 może zawierać domyślne ciała metod, ale nadal nie może mieć pól instancji ani konstruktorów.

Czym różnią się metody abstract i virtual w C#?

Metoda virtual ma ciało, które klasy pochodne mogą nadpisać. Metoda abstract nie ma ciała i klasy pochodne muszą ją nadpisać, inaczej same są abstrakcyjne (dla klasy konkretnej błąd CS0534). Metody abstrakcyjne mogą występować tylko w klasach abstrakcyjnych.

Czy klasa abstrakcyjna może mieć konstruktor w C#?

Tak. Uruchamia się podczas tworzenia klasy pochodnej, przez : base(...), i zwykle inicjalizuje wspólne pola. Zadeklaruj go jako protected: publiczny konstruktor w klasie abstrakcyjnej sugeruje, że kod zewnętrzny może go wywołać, a to niemożliwe.

Dlaczego nie mogę utworzyć instancji klasy abstrakcyjnej?

Bo może mieć abstrakcyjne składowe bez implementacji: wywołanie new Shape().Area() nie miałoby żadnego kodu do wykonania. Kompilator zgłasza CS0144 ("Cannot create an instance of the abstract type or interface"). Utwórz instancję konkretnej klasy pochodnej i, jeśli chcesz, trzymaj ją w zmiennej typu abstrakcyjnego: Shape s = new Circle(2);.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ