Dziedziczenie i polimorfizm — różne urządzenia, wspólne polecenie
Drukarka i skaner są urządzeniami, ale inaczej opisują swoje zadania. Wspólny typ pozwoli obsługiwać je w jednej pętli.
Czego się dziś nauczysz
- Utworzysz klasę pochodną.
- Użyjesz virtual i override.
- Wyjaśnisz wybór metody podczas działania.
Przygotowanie: lekcje 01–44. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Po co ten mechanizm? Kod przed i po
Problem: dwa rodzaje urządzeń kopiują wspólny kod
Drukarka i skaner mają nazwę oraz potrafią przedstawić się użytkownikowi. Zmiana formatu opisu wymaga teraz poprawienia dwóch metod.
Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program
uruchamiaj w oddzielnym projekcie konsolowym.
using System;
class Drukarka
{
public string Nazwa { get; }
public Drukarka(string nazwa) { Nazwa = nazwa; }
public string Opis() { return $"Urzadzenie: {Nazwa}"; }
}
class Skaner
{
public string Nazwa { get; }
public Skaner(string nazwa) { Nazwa = nazwa; }
public string Opis() { return $"Urzadzenie: {Nazwa}"; }
}
class Program
{
static void Main()
{
Console.WriteLine(new Drukarka("D1").Opis());
Console.WriteLine(new Skaner("S1").Opis());
}
}
Urzadzenie: D1
Urzadzenie: S1
Po: wspólna część w bazie
Dwukropek oznacza dziedziczenie. Drukarka jest Urzadzeniem, więc może korzystać ze wspólnego Opis. Konstruktor pochodny przekazuje nazwę do bazy.
Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program
uruchamiaj w oddzielnym projekcie konsolowym.
using System;
class Urzadzenie
{
public string Nazwa { get; }
public Urzadzenie(string nazwa) { Nazwa = nazwa; }
public string Opis() { return $"Urzadzenie: {Nazwa}"; }
}
class Drukarka : Urzadzenie
{
public Drukarka(string nazwa) : base(nazwa) { }
}
class Skaner : Urzadzenie
{
public Skaner(string nazwa) : base(nazwa) { }
}
class Program
{
static void Main()
{
Console.WriteLine(new Drukarka("D1").Opis());
Console.WriteLine(new Skaner("S1").Opis());
}
}
Urzadzenie: D1
Urzadzenie: S1
1. Wersja przed zmianą
Nazwa i Opis pojawiają się w dwóch niezależnych klasach.
2. Wersja po zmianie
Wspólna właściwość i opis są zadeklarowane raz w Urzadzeniu.
3. Co konkretnie zyskaliśmy?
Zmiana napisu Urzadzenie na Sprzet odbywa się w jednym miejscu. Klasa pochodna może później dodać własne zachowanie. Base(nazwa) przygotowuje bazową część tego samego obiektu; nie powstaje dodatkowe urządzenie.
4. Kiedy ten mechanizm jest zbędny?
Dziedziczenie wiąże klasy. Sama jedna podobna linia kodu nie uzasadnia hierarchii. Jeśli obiekt MA inny obiekt, jak drukarka ma pojemnik z tuszem, stosuj składanie. Gdy typy różnią się tylko danymi, czasem wystarczy jedna klasa i różne obiekty.
Ważne reguły
Klasa ma jedną bezpośrednią klasę bazową. Konstruktorów nie dziedziczy — może je wywołać przez base. Prywatne dane bazy są częścią stanu obiektu, lecz klasa pochodna nie ma do nich bezpośredniego dostępu. Sealed class zamyka możliwość dziedziczenia. Console.WriteLine(string) wypisuje gotowy opis i zwraca void; Opis i konstruktory są naszym kodem.
Polimorfizm: przed użyciem wspólnej operacji
Rozważ funkcję z warunkami: dla rodzaju „roboty” zwraca jeden napis, dla „quiz” drugi. Jeśli podobne rozgałęzienie pojawia się przy uruchomieniu, zatrzymaniu i opisie, dodanie atrakcji wymaga zmian w wielu miejscach.
Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program
uruchamiaj w oddzielnym projekcie konsolowym.
using System;
class Program
{
static string Uruchom(string rodzaj)
{
if (rodzaj == "roboty") return "Roboty ruszaja";
if (rodzaj == "quiz") return "Pytanie na ekranie";
return "Otwarcie stanowiska";
}
static void Main()
{
string[] rodzaje = { "roboty", "quiz", "zwykla" };
foreach (string rodzaj in rodzaje) Console.WriteLine(Uruchom(rodzaj));
}
}
Roboty ruszaja
Pytanie na ekranie
Otwarcie stanowiska
Po: przejdź do osobnej lekcji o polimorfizmie
Wersja po zmianie daje identyczny wynik, ale umieszcza zachowania w klasach Roboty i Quiz. Pętla wywołuje Uruchom bez sprawdzania rodzaju. To konkretna korzyść override. Nie skraca to koniecznie kodu — zmienia miejsce, w którym dodajesz nowe zachowanie.
Wyjaśnienie krok po kroku
Relacja „jest rodzajem”
Drukarka : Urzadzenie oznacza, że Drukarka dziedziczy po Urzadzenie. Klasa w C# może bezpośrednio dziedziczyć po jednej klasie. Dziedziczenie nie jest dobrym modelem dla relacji „ma”: drukarka ma toner, ale nie jest tonerem.
Tak wygląda to w kodzie — Definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Urzadzenie { }
class Drukarka : Urzadzenie { }
Drukarka jest rodzajem Urzadzenia. Dwukropek wskazuje klasę bazową; nie oznacza pola przechowującego drugie urządzenie.
Virtual i override
virtual w klasie bazowej zezwala na zmianę zachowania w klasie pochodnej. override podaje nową wersję. Przy wywołaniu przez zmienną typu bazowego zostanie wybrana wersja zgodna z rzeczywistym typem obiektu.
Tak wygląda to w kodzie — Definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Urzadzenie
{
public virtual string Start() { return "Start"; }
}
class Drukarka : Urzadzenie
{
public override string Start() { return "Drukowanie"; }
}
Virtual udostępnia wersję domyślną. Override określa zachowanie drukarki przy wywołaniu tej samej operacji.
Abstract i kompozycja
Klasa abstract nie pozwala tworzyć bezpośrednich obiektów przez new. Metoda abstract nie ma ciała i musi zostać zaimplementowana w konkretnej klasie pochodnej. Jeśli klasy mają tylko współpracować, często prostsze jest przechowywanie jednego obiektu jako właściwości drugiego.
Tak wygląda to w kodzie — Definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Bateria { }
class Robot
{
private Bateria bateria = new Bateria();
}
Robot MA baterię, dlatego przechowuje ją w polu. Zapis Robot : Bateria oznaczałby inną relację: „robot jest baterią”.
Przykład do zbadania
Wklej cały przykład do nowego projektu konsolowego jako Program.cs. Zastąp poprzednią zawartość pliku. Nie łącz kilku całych programów w jednym pliku.
using System;
class Urzadzenie
{
public virtual string Opis() { return "Urzadzenie"; }
}
class Drukarka : Urzadzenie
{
public override string Opis() { return "Drukuje dokument"; }
}
class Skaner : Urzadzenie
{
public override string Opis() { return "Skanuje dokument"; }
}
class Program
{
static void Main()
{
Urzadzenie[] sprzet = { new Drukarka(), new Skaner() };
foreach (Urzadzenie element in sprzet) Console.WriteLine(element.Opis());
}
}
Drukuje dokument
Skanuje dokument
Co dzieje się po kolei
- Tablica przechowuje referencje wspólnego typu Urzadzenie.
- Pętla nie pyta, czy element jest drukarką; wywołuje wspólną metodę.
- override sprawia, że wykonanie zależy od obiektu, choć zmienna ma typ bazowy.
Użyte w tej lekcji elementy
Metody .NET są gotowymi narzędziami biblioteki. Instrukcje języka i nasze metody oznaczamy osobno. Właściwości, np. Length, odczytujemy bez nawiasów.
| Element | Wynik | Działanie i ograniczenia |
|---|---|---|
element.Opis() |
string | Nasza metoda wirtualna zwracająca opis działania. |
Console.WriteLine(tekst) |
void | Wyświetla wynik zwrócony przez Opis. |
Częste błędy
- Samo powtórzenie nazwy metody nie jest poprawnym zastępstwem override.
- Dziedziczenie nie daje dostępu do prywatnych elementów klasy bazowej.
- Nie twórz hierarchii tylko po to, żeby uniknąć dwóch powtórzonych linii.
Zadania
Dodaj Projektor dziedziczący po Urzadzenie i zwracający Wyświetla obraz.
Sprawdź: Po dopisaniu obiektu tablica wyświetla trzy opisy.
Zapisz Urzadzenie u = new Drukarka(); i wywołaj Opis.
Sprawdź: Wynik: Drukuje dokument.
Ustal relację między Samochod, Pojazd i Silnik.
Sprawdź: Samochod jest Pojazdem i ma Silnik.
Sprawdź, czy rozumiesz
Wyjaśnij własnymi słowami, jak działa główny przykład i dlaczego daje taki wynik. Następnie zmień jedną daną i przewidź wynik przed uruchomieniem. Wróć do celów lekcji i sprawdź, które potrafisz już wykonać samodzielnie.
Dokumentacja: Microsoft Learn — temat tej lekcji.