1. Wprowadzenie
Wyobraź sobie, że mamy apkę do ewidencji zwierzaków w zoo (ten sam przykład, który zaczęliśmy rozwijać na poprzednich wykładach). Stworzyliśmy bazową klasę Animal z metodą MakeSound, żeby każde zwierzę mogło "wydawać" jakiś dźwięk.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Zwierzę wydaje dźwięk.");
}
}
Ale nagle okazuje się, że lew i papuga brzmią zupełnie inaczej. Uniwersalne "Zwierzę wydaje dźwięk" już nie pasuje. I tu wchodzi do gry nadpisywanie metod. Potrzebujesz, żeby każdy potomek brzmiał po swojemu!
2. Składnia: jak działa override
Podstawowe zasady
- Żeby nadpisać metodę, musi być oznaczona w klasie bazowej jako virtual (albo abstract).
- W klasie pochodnej używasz słowa kluczowego override przed metodą o tej samej sygnaturze.
Przykład: lwy i papugi
public class Lion : Animal
{
public override void MakeSound()
{
Console.WriteLine("Rrrrr!");
}
}
public class Parrot : Animal
{
public override void MakeSound()
{
Console.WriteLine("Papuga głupek!");
}
}
Teraz, jeśli stworzysz listę zwierzaków i wywołasz dla każdego MakeSound, każdy "powie" coś swojego:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Animal()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
// Wynik:
// Rrrrr!
// Papuga głupek!
// Zwierzę wydaje dźwięk.
3. Wirtualna tablica wywołań (v-table)
Gdy używasz virtual i override, kompilator generuje dla klasy specjalną "wirtualną tablicę metod" (v-table).
Przy wywołaniu metody przez referencję do klasy bazowej CLR patrzy w tabelę: czy ta metoda nie została nadpisana w potomku? Jeśli tak — wywołuje wersję z potomka.
To właśnie ta "magia" późnego wiązania (late binding), czyli dynamicznego polimorfizmu.
4. Łączymy wszystko: rozwijamy naszą apkę
Dodajmy jeszcze jedną klasę:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Hau-hau!");
}
}
I użyjmy tego w naszej mini-apce do ewidencji zwierzaków:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Dog()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
Teraz rozbudowa i utrzymanie kodu to czysta przyjemność, a dodawanie nowych zwierzaków — sama radocha!
5. Nadpisywanie właściwości i override ze zwracaniem wartości
Metody to nie jedyne, co można nadpisywać. Możesz też nadpisywać wirtualne właściwości i indeksatory.
public class Animal
{
public virtual string Name { get; set; } = "Zwierzę";
}
public class Lion : Animal
{
public override string Name { get; set; } = "Lew";
}
To mega wygodne, gdy trzeba doprecyzować info dla konkretnego gatunku.
6. Implementacja bazowa przez base
Czasem chcesz rozszerzyć zachowanie metody bazowej, a nie całkiem ją zastąpić.
Wtedy wywołujesz implementację bazową przez słowo kluczowe base wewnątrz nadpisanej metody.
public class Parrot : Animal
{
public override void MakeSound()
{
base.MakeSound(); // "Zwierzę wydaje dźwięk."
Console.WriteLine("Papuga głupek!");
}
}
W tym przypadku papuga najpierw wyda ogólny "zoo-dźwięk", a potem dorzuci swoje firmowe "Papuga głupek!".
7. Praktyczne zastosowania: gdzie to się przydaje
Ogarnianie mechanizmu override to must-have praktycznie w każdym poważnym projekcie C#, gdzie używasz OOP.
- Modele zwierzaków w naszym zoo? Już zrobione.
- Tworzenie customowych kontrolerów do UI: Nadpisujesz standardowe metody wizualne, dodając swoją logikę.
- Elastyczna logika biznesowa: Pozwala budować "szkielet" zachowania w klasach bazowych, a szczegóły ogarniać w potomkach.
- Testowanie i mocki: Łatwo "podmienić" metody przez potomków do unit-testów.
- Pluginy i rozszerzenia: Interfejs albo abstrakcyjna klasa bazowa, wiele implementacji — i wszystko działa dzięki poprawnemu nadpisywaniu.
8. sealed override i wirtualne metody w łańcuchach
Jeśli nie chcesz, żeby ktoś dalej w hierarchii nadpisywał twoją nadpisaną metodę, możesz użyć sealed override:
public class Base
{
public virtual void Foo() { }
}
public class Middle : Base
{
public sealed override void Foo() { }
}
public class Last : Middle
{
public override void Foo() {} // Błąd — nie można nadpisać!
}
To pozwala "zamknąć" łańcuch nadpisywań tam, gdzie to krytyczne dla poprawnego działania systemu.
9. Nowa metoda (słowo kluczowe new)
Czasem chcesz zadeklarować w potomku metodę o tej samej sygnaturze, ale bazowa nie jest virtual.
Wtedy możesz użyć słowa kluczowego new — ale to nie jest polimorfizm, tylko taka "maskarada":
public class Animal
{
public void MakeSound()
{
Console.WriteLine("Jestem zwierzęciem!");
}
}
public class Cat : Animal
{
public new void MakeSound()
{
Console.WriteLine("Jestem kotkiem!");
}
}
Animal animal = new Cat();
animal.MakeSound(); // "Jestem zwierzęciem!"
Cat cat = new Cat();
cat.MakeSound(); // "Jestem kotkiem!"
Tutaj wszystko zależy od typu zmiennej w momencie wywołania! Dlatego do prawdziwego dynamicznego polimorfizmu zawsze używaj combo virtual + override.
10. Typowe błędy przy nadpisywaniu metod
Błąd nr 1: próba nadpisania metody, która nie była oznaczona jako virtual, abstract albo override.
W C# nie można nadpisywać zwykłych metod. Jeśli metoda w klasie bazowej nie ma specjalnego modyfikatora (virtual, abstract albo override), próba napisania override w klasie pochodnej skończy się błędem kompilacji.
Błąd nr 2: niezgodność sygnatury metody.
Żeby metoda była naprawdę nadpisana, jej nazwa, typ zwracany i parametry muszą się w 100% zgadzać z metodą w klasie bazowej. Najmniejsza różnica (np. inny typ parametru) spowoduje utworzenie nowej metody, a nie nadpisanie.
Błąd nr 3: zapomniany modyfikator override.
Jeśli zdefiniujesz metodę o tej samej sygnaturze jak w klasie bazowej, ale nie dasz override, kompilator nie uzna tego za nadpisanie. To się nazywa ukrycie metody (będzie omówione osobno). W takim przypadku wywołanie metody przez zmienną typu bazowego da nieoczekiwany efekt — zostanie wywołana metoda bazowa, a nie twoja.
GO TO FULL VERSION