Dziedziczenie w C# — base, virtual/override i pułapka słowa new
public class Pracownik
{
public string Imie;
public decimal ObliczWynagrodzenie() => 5000m;
}
public class Kierownik
{
public string Imie; // ❌ powtórzone pole
public decimal ObliczWynagrodzenie() => 5000m; // ❌ powtórzona logika
public decimal Premia() => 1000m;
}
Dwie klasy, ten sam kod skopiowany w dwóch miejscach. Zmiana sposobu liczenia ObliczWynagrodzenie wymaga teraz pamiętania o edycji obu klas — a przy dziesięciu podobnych typach pracowników o dziesięciu. Dziedziczenie pozwala napisać wspólny kod raz, w jednym miejscu.
Podstawowa składnia
public class Pracownik
{
public string Imie;
public virtual decimal ObliczWynagrodzenie() => 5000m;
}
public class Kierownik : Pracownik // ':' oznacza dziedziczenie
{
public override decimal ObliczWynagrodzenie() => base.ObliczWynagrodzenie() + 1000m; // premia kierownicza
}
Kierownik dziedziczy po Pracownik — automatycznie ma pole Imie i metodę ObliczWynagrodzenie, bez przepisywania ich. override pozwala dostarczyć własną wersję metody, a base.ObliczWynagrodzenie() wywołuje oryginalną implementację z klasy bazowej, zamiast pisać jej logikę od nowa.
Konstruktory — zawsze wywołują konstruktor bazowy
public class Pracownik
{
public string Imie;
public Pracownik(string imie) => Imie = imie; // brak konstruktora bezparametrowego
}
public class Kierownik : Pracownik
{
public string Dzial;
public Kierownik(string imie, string dzial) : base(imie) // ✅ jawne wywołanie konstruktora bazowego
{
Dzial = dzial;
}
}
Jeśli klasa bazowa nie ma konstruktora bezparametrowego, każda klasa dziedzicząca musi jawnie wywołać jeden z jej konstruktorów przez : base(...) — inaczej kod się nie skompiluje. To nie ograniczenie, tylko gwarancja: obiekt Kierownik nie może istnieć bez poprawnie zainicjalizowanego Imie, bo to część kontraktu klasy bazowej.
virtual / override kontra zwykłe przesłonięcie
public class Pracownik
{
public decimal ObliczWynagrodzenie() => 5000m; // BEZ virtual
}
public class Kierownik : Pracownik
{
public new decimal ObliczWynagrodzenie() => 8000m; // 'new' zamiast 'override'
}
To się skompiluje — ale nie robi tego, czego się spodziewasz. new nie nadpisuje metody, tylko ją ukrywa — która wersja się wykona, zależy od tego, przez jaki typ referencji wywołujesz metodę, nie od rzeczywistego typu obiektu:
Kierownik kierownik = new Kierownik();
Pracownik jakoPracownik = kierownik; // ta sama instancja, inny typ referencji
Console.WriteLine(kierownik.ObliczWynagrodzenie()); // 8000 — wersja Kierownika
Console.WriteLine(jakoPracownik.ObliczWynagrodzenie()); // 5000 — wersja Pracownika! Ten sam obiekt!
To jedna z najbardziej mylących pułapek dziedziczenia w C# — ten sam obiekt zwraca różne wyniki w zależności od typu zmiennej, przez którą go widzisz. Poprawne rozwiązanie to virtual w klasie bazowej i override w pochodnej:
public class Pracownik
{
public virtual decimal ObliczWynagrodzenie() => 5000m; // 'virtual' — może być nadpisana
}
public class Kierownik : Pracownik
{
public override decimal ObliczWynagrodzenie() => 8000m; // 'override' — realne nadpisanie
}
Pracownik jakoPracownik = new Kierownik();
Console.WriteLine(jakoPracownik.ObliczWynagrodzenie()); // 8000 — teraz zawsze wersja rzeczywistego typu
Z virtual/override wynik zależy od rzeczywistego typu obiektu w pamięci, nie od typu zmiennej — to jest polimorfizm, i to jest zachowanie, którego programiści intuicyjnie oczekują.
protected — widoczne dla podklas, ukryte dla reszty świata
public class Pracownik
{
protected decimal PodstawaWynagrodzenia = 5000m; // widoczne w Pracownik i jego podklasach
public virtual decimal ObliczWynagrodzenie() => PodstawaWynagrodzenia;
}
public class Kierownik : Pracownik
{
public override decimal ObliczWynagrodzenie() => PodstawaWynagrodzenia + 1000m; // ✅ ma dostęp
}
// gdzieś indziej w kodzie:
var k = new Kierownik();
var x = k.PodstawaWynagrodzenia; // ❌ błąd kompilacji — protected nie jest widoczne z zewnątrz
protected to trzeci poziom dostępu obok public i private — pole albo metoda jest widoczna dla klasy, w której jest zdefiniowana, i dla wszystkich klas dziedziczących, ale niewidoczna dla reszty kodu. To naturalny wybór dla elementów, które podklasy powinny móc wykorzystać lub nadpisać, ale które nie powinny być częścią publicznego API klasy.
sealed — blokada dalszego dziedziczenia
public sealed class RaportKoncowy : Pracownik
{
// ...
}
public class Nadpisanie : RaportKoncowy { } // ❌ błąd kompilacji — RaportKoncowy jest sealed
sealed na klasie zabrania dziedziczenia po niej — przydatne, gdy chcesz zagwarantować, że zachowanie klasy nie zostanie zmienione przez podklasę (bezpieczeństwo, przewidywalność) albo gdy klasa nie została zaprojektowana z myślą o rozszerzaniu. sealed można też dodać do pojedynczej nadpisanej metody, blokując jej dalsze nadpisywanie w kolejnych poziomach dziedziczenia:
public override sealed decimal ObliczWynagrodzenie() => PodstawaWynagrodzenia + 1000m;
// klasy dziedziczące po Kierowniku nie mogą już nadpisać tej metody
Kiedy NIE używać dziedziczenia
Dziedziczenie modeluje relację “jest-czymś” (Kierownik jest Pracownikiem). Jeśli relacja to raczej “posiada” albo “korzysta z” (samochód ma silnik, ale nim nie jest), dziedziczenie to zły wybór — prowadzi do sztywnych, głębokich hierarchii trudnych do zmiany. W takich przypadkach kompozycja (obiekt trzymający referencję do innego obiektu jako pole) jest zwykle elastyczniejsza — to temat sam w sobie, ale warto zapamiętać regułę: dziedzicz, gdy relacja to “jest-czymś” z prawdziwie współdzielonym zachowaniem; komponuj, gdy to “korzysta z”.
// ❌ Dziedziczenie tam, gdzie relacja to "ma", nie "jest"
public class SamochodZly : Silnik { } // samochód NIE JEST silnikiem
// ✅ Kompozycja — samochód MA silnik
public class Samochod
{
private readonly Silnik _silnik; // pole, nie klasa bazowa
public Samochod(Silnik silnik) => _silnik = silnik;
public void Uruchom() => _silnik.Wystartuj();
}
Kompozycja daje też coś, czego dziedziczenie nie potrafi łatwo: Silnik można podmienić w runtime (inny obiekt przekazany do konstruktora) — z dziedziczeniem klasa bazowa jest ustalona raz, na stałe, przy kompilacji.
Zobacz to w praktyce
Ten artykuł pokazał składnię i zasady na małych przykładach. Żeby zobaczyć dziedziczenie i polimorfizm w akcji na większym, realnym projekcie (system klas postaci w grze RPG — Wojownik, Łucznik, Mag dziedziczące po wspólnym Bohater), sprawdź Dziedziczenie i Polimorfizm w Grze RPG z serii Clean Architecture RPG.
Podsumowanie
Dziedziczenie eliminuje powtórzony kod między klasami modelującymi tę samą, szerszą kategorię bytów. Klucz do poprawnego polimorfizmu to virtual w klasie bazowej i override w pochodnej — słowo new kompiluje się, ale ukrywa metodę zamiast ją nadpisywać, dając różne wyniki w zależności od typu zmiennej, nie rzeczywistego typu obiektu. protected udostępnia szczegóły implementacji podklasom bez wystawiania ich na zewnątrz, a sealed świadomie blokuje dalsze rozszerzanie, gdy to część projektu, nie przeoczenie.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
1 comment