1. Wprowadzenie
Jeśli trochę się spociłeś na dźwięk słowa nadpisywanie, spokojnie — to nie jest straszne, a wręcz bardzo wygodne! Nadpisywanie metody to możliwość podmienienia zachowania metody klasy bazowej na własne w klasie pochodnej. Dzięki temu nasz kod jest elastyczny, rozszerzalny i gotowy na prawdziwe życie, gdzie każde zwierzę na pewno nie chce być tylko "jakimś dźwiękiem".
Żeby pozwolić na nadpisanie metody, klasa bazowa oznacza ją słowem kluczowym virtual. Klasa pochodna, żeby podmienić implementację, używa słowa kluczowego override.
Przykład
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("Jakiś uniwersalny dźwięk zwierzęcia...");
}
}
A teraz w klasie psa:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Hau-hau!");
}
}
- W klasie bazowej — virtual.
- W klasie pochodnej — override.
- Sygnatura metody (nazwa, typ zwracany, parametry) musi się zgadzać.
Schemat wizualny
2. Demonstracja działania
Zobaczmy, jak to działa w praktyce. Tworzymy zwierzaki i sprawdzamy dźwięk:
Animal pet1 = new Animal { Name = "Bez nazwy" };
Dog pet2 = new Dog { Name = "Barbos" };
pet1.MakeSound(); // Wypisze: Jakiś uniwersalny dźwięk zwierzęcia...
pet2.MakeSound(); // Wypisze: Hau-hau!
Teraz trochę trudniej:
Co jeśli psa zapiszemy do zmiennej typu Animal?
Animal pet3 = new Dog { Name = "Szarik" };
pet3.MakeSound(); // ???
Jak myślisz, co się stanie?
Odpowiedź: Wypisze "Hau-hau!"
Bo nawet jeśli zmienna jest typu Animal, to "wskazuje" na psa i wywoła się nadpisana wersja metody!
Oto magia dynamicznego (albo późnego) wiązania.
3. Użycie słowa kluczowego base przy nadpisywaniu
Czasem nie chcemy całkiem podmienić implementacji metody, tylko ją rozszerzyć — np. dodać coś swojego, a potem jeszcze wykonać stare zachowanie. Do tego służy słowo kluczowe base. Pozwala wywołać wersję metody z klasy bazowej.
public class Cat : Animal
{
public override void MakeSound()
{
base.MakeSound(); // wywołanie bazowej implementacji
Console.WriteLine("Miau!");
}
}
Przy wywołaniu tej metody najpierw pojawi się "Jakiś uniwersalny dźwięk zwierzęcia...", a potem "Miau!"
4. Jak działa wybór metody przy nadpisywaniu
Żeby lepiej zrozumieć, co się dzieje "pod maską", wyobraź sobie taką tabelkę (wirtualna dyspozycja):
| Typ zmiennej | Typ obiektu | Jaka metoda się wywoła |
|---|---|---|
| Animal | Animal | Animal.MakeSound |
| Animal | Dog | Dog.MakeSound |
| Animal | Labrador | Labrador.MakeSound |
| Dog | Labrador | Labrador.MakeSound |
| Dog | Dog | Dog.MakeSound |
Najważniejsza zasada:
Typ zmiennej jest ważny tylko dla kompilatora, a przy wykonaniu liczy się typ faktycznego obiektu (to, co stworzyliśmy przez new).
Ten mechanizm nazywa się dynamicznym (albo późnym) wiązaniem — właśnie na tym opiera się polimorfizm (o tym — w następnym wykładzie!).
Po co nadpisywać metody
- W GUI-frameworkach: masz klasę bazową okna i nadpisujesz metody do rysowania konkretnych elementów.
- W silnikach gier: klasa bazowa Enemy, a klasy dziedziczące robią różne typy zachowań.
- W unit-testach: możesz tworzyć "atrapy" (stubs, mocks) dla metod.
Nowoczesne frameworki .NET mocno korzystają z tego mechanizmu do eventów, kodu szablonowego, dziedziczenia konfiguracji, a nawet serializacji obiektów (np. przez virtual properties).
5. Słowo kluczowe new przy ukrywaniu metod
Już wiemy, że do nadpisywania metod potrzebny jest duet virtual/override. Ale w C# jest jeszcze jeden modyfikator związany z metodami w hierarchii dziedziczenia — to new.
Po co jest new?
new używasz, jeśli w klasie pochodnej deklarujesz metodę o tej samej sygnaturze co w bazowej, ALE nie chcesz nadpisywać metody wirtualnej, tylko właśnie ukryć (zamaskować) bazową metodę.
- To nie jest nadpisywanie, tylko ukrywanie.
- Taka metoda wywołuje się po typie zmiennej, a nie po faktycznym typie obiektu (nie ma dynamicznego polimorfizmu!).
- Kompilator ostrzeże, jeśli "przypadkiem" ukryjesz metodę bez słowa new.
Przykład: różnica override i new
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Zwierzę wydaje jakiś dźwięk...");
}
}
public class Dog : Animal
{
// Ukrywamy metodę klasy bazowej (NIE nadpisujemy)
public new void MakeSound()
{
Console.WriteLine("To nie override! Po prostu psia metoda.");
}
}
Teraz zobaczmy zachowanie:
Animal a = new Dog();
Dog d = new Dog();
a.MakeSound(); // "Zwierzę wydaje jakiś dźwięk..."
d.MakeSound(); // "To nie override! Po prostu psia metoda."
- Jeśli zmienna jest typu Dog — wywoła się metoda z Dog.
- Jeśli zmienna jest typu Animal, nawet jeśli w niej jest Dog — wywoła się metoda Animal!
6. Feedback i szczegóły implementacji
Na początku programowania bardzo często pojawia się nieporozumienie, gdy metoda niby jest "nadpisana", ale jakoś działa po staremu. Zwykle powód jest prosty: w klasie bazowej nie ma virtual, albo w pochodnej metoda jest zadeklarowana z new, a nie z override. Drugi przypadek jest szczególnie podstępny — jeśli wywołasz metodę przez zmienną typu bazowego, wywoła się bazowa wersja, a nie nadpisana. Więc zawsze pilnuj, żeby dobrze używać słów kluczowych.
Poza błędami składniowymi, czasem początkujący próbują zmienić typ zwracany przy nadpisywaniu. Na przykład zrobić w bazowej funkcji typ object, a w pochodnej — string. Tak nie można: sygnatura metody musi być identyczna.
Tabela porównawcza: override vs new
| Cecha | override | new |
|---|---|---|
| Mechanizm | Nadpisuje metodę wirtualną | Ukrywa metodę klasy bazowej |
| Późne wiązanie | Tak — działa przez dynamiczny polimorfizm | Nie — działa po typie zmiennej |
| Wymaga, żeby w base było... | virtual, abstract albo już override | Nie |
| Zalecane użycie | Tak, prawie zawsze | Tylko w wyjątkowych przypadkach |
7. Działanie nadpisanych metod w hierarchiach
Cała ta historia z nadpisywaniem robi się szczególnie ciekawa, jeśli mamy długie łańcuchy dziedziczenia:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Zwierzę coś robi...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Hau-hau!");
}
}
public class Labrador : Dog
{
public override void MakeSound()
{
Console.WriteLine("Jestem labrador: łau-łau!");
}
}
Co się stanie, jeśli napiszemy:
Animal pet = new Labrador();
pet.MakeSound(); // => "Jestem labrador: łau-łau!"
C# zawsze wybierze najgłębszą implementację metody wirtualnej, która jest na danym obiekcie.
8. Typowe błędy przy nadpisywaniu metod
Świat nie jest idealny i studenci (i doświadczeni programiści!) czasem popełniają błędy. Nauczmy się od razu unikać najpopularniejszych "min":
1. Zapomniane virtual w klasie bazowej
public class Animal
{
public void MakeSound() { ... } // Brak 'virtual'
}
public class Dog : Animal
{
// Błąd kompilacji! Nie możemy nadpisać.
public override void MakeSound()
{
Console.WriteLine("Hau!");
}
}
C# od razu powie: 'Dog.MakeSound()': no suitable method found to override
2. Sygnatury się nie zgadzają
Upewnij się, że nazwa metody, typ zwracany i parametry są identyczne:
public class Animal
{
public virtual void MakeSound() { ... }
}
public class Dog : Animal
{
// Błąd: sygnatura inna (np. dodany parametr)
public override void MakeSound(string sound)
{
Console.WriteLine(sound);
}
}
3. Nie używaj new zamiast override bez potrzeby
Słowo kluczowe new pozwala ukryć metodę klasy bazowej, ale to nie jest nadpisywanie i nie działa przez dynamiczny polimorfizm. To inny mechanizm, którego zwykle lepiej unikać bez mocnego powodu.
GO TO FULL VERSION