1. Wprowadzenie
W tym wykładzie ogarniemy, jak w C# działa składnia dziedziczenia: nauczysz się deklarować klasy pochodne, zrozumiesz, czym jest słowo kluczowe base i jak dzięki niemu odwoływać się do klasy rodzica. To nie tylko ułatwi ci życie podczas pisania kodu, ale też sprawi, że program będzie dużo bardziej elegancki (a czasem nawet pozwoli ci się wyspać — bo nie będziesz musiał poprawiać błędów z kopiowania kodu).
Prosty przykład z życia: mamy klasę Vehicle (pojazd) — bazowy, uniwersalny szablon. Wszystkie pojazdy mogą jechać, mają kolor, producenta itd.:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public void Drive()
{
Console.WriteLine("Jedziemy!");
}
}
A teraz chcemy dodać nowy typ pojazdu — samochód. Samochód to wciąż pojazd, ale ma swoje cechy (np. liczbę drzwi). Byłoby dziwne znowu pisać wszystko o kolorze i producencie, skoro to już jest zdefiniowane!
Dziedziczenie pozwala nam powiedzieć: "Samochód to pojazd, tylko z dodatkowymi bajerami."
2. Jak zadeklarować klasę potomną
W C# do stworzenia klasy dziedziczącej używamy dwukropka :. Po nazwie klasy podajemy, po kim dziedziczymy.
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}
Czytaj tak: klasa Car dziedziczy wszystkie pola i metody klasy Vehicle i dodaje swoje własne bajery — na przykład liczbę drzwi.
Uwaga: w C# mamy tylko pojedyncze dziedziczenie klas (czyli klasa może dziedziczyć tylko JEDNĄ klasę bazową). Ale interfejsów możesz mieć ile chcesz! (o tym pogadamy później).
Jak to działa?
Kiedy deklarujesz:
Car myCar = new Car();
...to twój obiekt myCar ma dostęp zarówno do wszystkich pól/metod Vehicle, jak i do swoich własnych właściwości. Przykład:
myCar.Brand = "Toyota";
myCar.Color = "Niebieski";
myCar.NumberOfDoors = 4;
myCar.Drive(); // Metoda odziedziczona
Realny scenariusz:
W naszym "rozwijającym się" programie można by zrobić menu pojazdów, gdzie użytkownik wybiera typ pojazdu, a potem program pokazuje wszystkie jego właściwości. Dodając nowy typ pojazdu, piszesz minimum kodu!
3. Dziedziczenie konstruktorów
Kiedy tworzymy obiekty klasy pochodnej, czasem chcemy zainicjować właściwości klasy bazowej przez konstruktor. Na przykład, żeby na pewno nie zapomnieć podać marki i koloru:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
}
Teraz w konstruktorze klasy dziedziczącej można (i trzeba) wywołać konstruktor klasy bazowej! Do tego służy słowo kluczowe base:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color) // wywołujemy konstruktor Vehicle!
{
NumberOfDoors = numberOfDoors;
}
}
Jak to wygląda w praktyce?
Car bmw = new Car("BMW", "Czarny", 4);
Console.WriteLine($"{bmw.Brand}, {bmw.Color}, drzwi: {bmw.NumberOfDoors}");
bmw.Drive(); // Nadal działa!
Wychodzi na to, że bmw — to i samochód, i pojazd jednocześnie. Pamięta o swoich "rodzicach".
4. Różnica między base a this
W C# są dwa słowa kluczowe do pracy z członkami klasy: this i base.
- this — odwołanie do bieżącego obiektu (np. jego własnego pola lub metody).
- base — odwołanie do członka klasy bazowej (czyli do rodzica).
Kiedy używać base? Wyobraź sobie, że chcesz rozszerzyć logikę metody z klasy bazowej, albo po prostu zainicjować klasę bazową z klasy pochodnej. Na przykład:
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int gears)
: base(brand, color) // wywołujemy konstruktor klasy bazowej
{
NumberOfGears = gears;
}
public void ShowInfo()
{
// Używamy właściwości z klasy bazowej!
Console.WriteLine($"{Brand} ({Color}), przełożeń: {NumberOfGears}");
}
}
5. Słowo kluczowe base w metodach
base — to nie tylko konstruktory! Czasem chcesz zmienić zachowanie metody z klasy bazowej, ale jednocześnie użyć kawałka jej logiki.
Załóżmy, że chcemy, żeby metoda Drive w klasie Car dodatkowo wypisywała, jakim autem jedziemy:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void DriveCar()
{
base.Drive(); // wywołujemy bazową implementację
Console.WriteLine($"Jedziemy {Brand} z {NumberOfDoors} drzwiami!");
}
}
Wyjaśnienie:
Wywołanie base.Drive(); — to jak powiedzieć: "Tato, ty najpierw opowiedz swoją część historii, a ja dodam szczegóły!". W efekcie po wywołaniu DriveCar() zostanie wypisane:
Jedziemy!
Jedziemy BMW z 4 drzwiami!
O nadpisywaniu metod (override) pogadamy w kolejnych wykładach :P
6. Schemat dziedziczenia
Zobaczmy wizualnie relacje między klasami. Oto prosty schemat (bo UML — straszna rzecz, zwłaszcza rano!):
Vehicle (bazowy)
/ \
Car Bicycle
- Wszystkie strzałki idą OD klasy pochodnej DO bazowej.
- Wszystkie klasy potomne mają dostęp do public/protected członków klasy bazowej.
Różnice między public, protected i private
Małe przypomnienie, żeby nie było zamieszania.
W klasie pochodnej widać:
| Modyfikator | Widoczność w potomku |
|---|---|
| public | Tak |
| protected | Tak |
| private | Nie |
Częsty błąd:
Próba odwołania się do private-pola z klasy potomnej:
public class Vehicle
{
private string secret = "Nikt się nie dowie!";
}
public class Car : Vehicle
{
public void RevealSecret()
{
Console.WriteLine(secret); // Błąd! Niewidoczne.
}
}
Porównanie: klasa bazowa vs. pochodna
| Vehicle | Car (potomek) | |
|---|---|---|
| Brand | jest | dziedziczy |
| Color | jest | dziedziczy |
| Drive() | jest | dziedziczy (lub rozszerza) |
| NumberOfDoors | nie | własne (unikalna właściwość) |
7. Przykład
Jeśli dalej rozwijamy proste aplikację „Ewidencja pojazdów”, możemy ją zbudować tak:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
public void Drive()
{
Console.WriteLine($"{Brand} {Color} jedzie!");
}
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void ShowCar()
{
Console.WriteLine($"Marka: {Brand}, Kolor: {Color}, Drzwi: {NumberOfDoors}");
}
}
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int numberOfGears)
: base(brand, color)
{
NumberOfGears = numberOfGears;
}
}
W głównej metodzie możesz stworzyć listę pojazdów i opisywać je na różne sposoby:
Car ford = new Car("Ford", "Czerwony", 4);
ford.ShowCar();
ford.Drive();
Bicycle trek = new Bicycle("Trek", "Zielony", 21);
trek.Drive();
8. Błędy, niuanse i trochę humoru
Bardzo częsty błąd — zapomnieć wywołać konstruktor bazowy w klasie potomnej. Jeśli Vehicle nie ma konstruktora bez parametrów, a nie wywołasz base(...), dostaniesz błąd kompilacji: kompilator będzie narzekał, że nie może zainicjować bazy. To jak próbować zbudować drugie piętro bez pierwszego. Nie da się. Trzeba przekazać dane w górę.
Drugi błąd: zapomnieć, że pola/metody klasy bazowej zadeklarowane jako private NIE SĄ WIDOCZNE w potomkach. Jeśli chcesz coś zostawić dla "dzieci", użyj protected (o tym więcej — w kolejnych wykładach).
I najważniejsze: dziedziczenie jest wygodne, ale nie buduj zbyt zagmatwanych hierarchii. W prawdziwym życiu, jeśli twoja drabinka dziedziczenia ma więcej niż dwa-trzy poziomy, coś pewnie poszło nie tak. Bardzo łatwo się pogubić, kto po kim dziedziczy i po co to wszystko było.
GO TO FULL VERSION