Dziedziczenie pozwala jednej klasie budować na drugiej. Klasa pochodna dostaje składowe klasy bazowej, dodaje własne i może zastąpić zachowanie, na którego zastąpienie pozwala klasa bazowa. W połączeniu z metodami virtual daje to polimorfizm: kod napisany dla klasy bazowej uruchamia właściwe zachowanie klasy pochodnej, nie wiedząc, z którą klasą pochodną ma do czynienia.
Tworzenie klasy pochodnej
Podaj klasę bazową po dwukropku. Klasa pochodna ma wszystko, co klasa bazowa, plus to, co sama deklaruje:
Wynik:
TX-19: 20 km, 2 fares
True
Co klasa pochodna dostaje, a czego nie:
- Dziedziczone: pola, właściwości, metody, zdarzenia i typy zagnieżdżone. Wszystko fizycznie jest w obiekcie.
- Dostępne: tylko to, na co pozwala klasa bazowa: składowe
public,protectediinternal. SkładowaprivateklasyVehicleistnieje w każdym obiekcieTaxi, ale kodTaxinie może się do niej odwołać. DlategoKmmaprivate set:Taximoże ją odczytać, a zmienia ją tylko przezDrive. - Niedziedziczone: konstruktory.
Taximusi zadeklarować własny i połączyć go z jednym z konstruktorówVehicleprzez: base(plate). Kolejność, w jakiej się wykonują, pokazuje strona o konstruktorach.
Każda klasa ostatecznie dziedziczy po object, dlatego każdy obiekt ma ToString(), Equals() i GetHashCode().
virtual i override
Klasa bazowa oznacza metodę jako virtual, aby powiedzieć "klasy pochodne mogą dostarczyć własną wersję". Klasa pochodna zastępuje ją przez override. Wewnątrz nadpisania base.Method() wywołuje wersję z klasy bazowej.
Wynik:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
Zmienna pętli ma typ Notification, a mimo to każdy obiekt renderuje się po swojemu. To jest polimorfizm: wywołanie n.Render(...) jest rozstrzygane w trakcie działania na podstawie faktycznego typu obiektu. Zauważ też, że base.Render w Email używa Channel, która sama jest wirtualna, więc metoda bazowa wypisuje email, a nie generic. Wirtualne wywołanie wewnątrz klasy bazowej nadal trafia do nadpisania.
Właściwości też mogą być wirtualne, co pokazuje Channel. Pola nie mogą.
Kompilator pilnuje obu słów kluczowych. Napisanie override przy metodzie, która nie jest virtual, to błąd CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). Pominięcie override, gdy metoda bazowa jest wirtualna, to tylko ostrzeżenie, ale całkowicie zmienia znaczenie, co pokazuje następna sekcja.
new a override: ukrywanie zamiast nadpisywania
Jeśli klasa pochodna deklaruje metodę o tej samej sygnaturze co metoda bazowa, ale bez override, to ukrywa metodę bazową. Kompilator ostrzega (CS0114 dla wirtualnej metody bazowej, w pozostałych przypadkach CS0108) i sugeruje słowo kluczowe new, które wycisza ostrzeżenie, ale zachowuje ukrywanie:
Wynik:
Sales report
Report
Draft report
b i c to obiekty tego samego rodzaju, a jednak wypisują różne tytuły. Przy new wybór metody zależy od typu zmiennej i zapada w czasie kompilacji. Kod, który traktuje raporty jako Report (lista, parametr metody, callback frameworka), nigdy nie zobaczy DraftReport.Title. Prawie nigdy nie o to chodzi. Do polimorfizmu używaj override; new istnieje głównie na wypadek, gdy klasa bazowa, której nie kontrolujesz, doda składową o nazwie kolidującej z twoją.
sealed
sealed przy klasie zabrania dziedziczenia po niej:
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string jest sealed, podobnie jak wiele typów frameworka. Przy nadpisaniu sealed zatrzymuje łańcuch na tym poziomie:
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
Zaprojektowanie klasy pod dziedziczenie wymaga pracy: trzeba zdecydować, co jest wirtualne, na czym klasy pochodne mogą polegać i w jakiej kolejności dzieją się rzeczy. Klasa, której tak nie projektowano, jest bezpieczniejsza jako sealed, a zapieczętowanie można później cofnąć bez psucia czyjegokolwiek kodu, natomiast odpieczętowania nie da się wycofać, gdy inni już dziedziczą po twojej klasie. Wywołania składowych klas sealed mogą też być nieco szybsze, bo środowisko uruchomieniowe wie, że nie istnieje żadne nadpisanie.
Jedna klasa bazowa, wiele interfejsów
Klasa w C# ma dokładnie jedną klasę bazową. class Admin : User, Employee to błąd CS1721 ("cannot have multiple base classes"). Klasa może jednak obok klasy bazowej implementować dowolną liczbę interfejsów:
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
Używaj klasy bazowej dla relacji "jest rodzajem czegoś i współdzieli z tym implementację", a interfejsów dla "potrafi". Gdy klasa bazowa istnieje tylko po to, by zmusić klasy pochodne do uzupełnienia niektórych metod, właściwym narzędziem jest klasa abstrakcyjna.
Rzutowanie w górę i w dół hierarchii
Obiektu pochodnego zawsze można użyć tam, gdzie oczekiwany jest jego typ bazowy. Takie rzutowanie w górę jest niejawne i nie może się nie udać. Kierunek odwrotny, rzutowanie w dół, wymaga jawnego rzutowania i kończy się błędem w trakcie działania, jeśli obiekt nie jest tego typu:
Wynik:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
Napisanie Dog d = pet; bez rzutowania to błąd kompilacji (CS0266: "An explicit conversion exists (are you missing a cast?)"), bo kompilator wie tylko, że pet to jakieś Animal. Wybieraj is ze zmienną, gdy obiekt może być innego typu, a zwykłe rzutowanie tylko wtedy, gdy każda inna możliwość byłaby błędem. Częste rzutowanie w dół to sygnał problemu w projekcie: zwykle oznacza, że zachowanie powinno być metodą wirtualną w klasie bazowej. Wszystkie formy is omawia strona o dopasowywaniu wzorców.
Dziedziczenie a kolekcje
Polimorfizm działa element po elemencie, ale generyczne kolekcje typu pochodnego nie są kolekcjami typu bazowego: List<Animal> animals = new List<Dog>(); się nie kompiluje, bo lista przyjmowałaby wtedy Cat. Widoki tylko do odczytu są kowariantne, więc IEnumerable<Animal> animals = new List<Dog>(); jest w porządku. Dlaczego, wyjaśnia strona o typach generycznych.
Typowe błędy
- Zapomniane
override. Metoda kompiluje się z ostrzeżeniem i po cichu ukrywa zamiast nadpisywać. Traktuj CS0114 jak błąd. - Robienie wszystkiego
virtual. Każda wirtualna składowa to obietnica dla klas pochodnych, kiedy jest wywoływana i co może zakładać. Oznaczaj tylko te punkty rozszerzeń, które naprawdę planujesz. - Głębokie hierarchie. Trzy lub cztery poziomy dziedziczenia utrudniają ustalenie, która wersja metody się wykona. Kompozycja, czyli klasa trzymająca inny obiekt i wywołująca go, jest często prostsza niż kolejny poziom.
- Wywoływanie metod wirtualnych z konstruktora. Nadpisanie wykonuje się przed ciałem konstruktora klasy pochodnej, więc każde pole przypisywane w tym ciele ma jeszcze wartość domyślną.
- Dziedziczenie tylko po to, by ponownie użyć kodu. Jeśli
Stackdziedziczy poList, kod wywołujący może wykonaćInsertw środek twojego stosu. Zamiast tego trzymajListw prywatnym polu.
Najczęściej zadawane pytania
Jak działa dziedziczenie w C#?
Klasa podaje jedną klasę bazową po dwukropku: class Dog : Animal. Klasa pochodna dostaje wszystkie składowe klasy bazowej (pola, właściwości, metody, zdarzenia), może dodać własne i nadpisać te, które klasa bazowa oznaczyła jako virtual. Konstruktory nie są dziedziczone, a składowe private, choć obecne w obiekcie, nie są dostępne z klasy pochodnej.
Czym różni się virtual od override w C#?
virtual stawia się przy metodzie klasy bazowej i oznacza "klasy pochodne mogą to zastąpić". override stawia się przy metodzie klasy pochodnej i to ono dokonuje zastąpienia. Potrzebne są oba: nadpisanie metody, która nie jest virtual, abstract ani już override, to błąd CS0506. Przy wywołaniu metody wirtualnej środowisko uruchomieniowe wykonuje wersję dla faktycznego typu obiektu, a nie typu zmiennej.
Czym różni się new od override w C#?
override zastępuje metodę bazową dla każdego wywołującego, nawet dla kodu, który trzyma obiekt przez zmienną typu klasy bazowej. new tylko ją ukrywa: kod, który widzi obiekt jako typ pochodny, wywołuje nową metodę, a kod, który widzi go jako typ bazowy, nadal wywołuje metodę bazową. Dlatego Base b = new Derived(); b.M(); wykonuje Derived.M przy override i Base.M przy new.
Czy C# obsługuje wielodziedziczenie?
Nie dla klas: klasa ma dokładnie jedną klasę bazową, a podanie dwóch to błąd CS1721. Klasa może natomiast implementować dowolną liczbę interfejsów i w ten sposób C# modeluje "ten typ potrafi kilka rzeczy". Od C# 8 interfejsy mogą też zawierać domyślne implementacje metod.
Co oznacza sealed w C#?
Klasa sealed nie może być klasą bazową; dziedziczenie po niej to błąd CS0509. Przykładowo string jest sealed. Przy metodzie sealed override uniemożliwia klasom niżej w hierarchii ponowne nadpisanie (CS0239). Zapieczętowanie klas, których nie projektowano z myślą o dziedziczeniu, to rozsądne ustawienie domyślne.
Jak wywołać metodę klasy bazowej w C#?
Użyj base.MethodName(...) w klasie pochodnej, zwykle wewnątrz nadpisania: public override string Describe() => base.Describe() + " with GPS";. W przypadku konstruktorów użyj : base(...) po liście parametrów, aby wybrać, który konstruktor bazowy się wykona.