1. Wprowadzenie
Wyobraź sobie, że masz bazową klasę Animal z metodą Speak(). Dla wszystkich zwierzaków domyślnie wypisuje "Jakiś dźwięk". Chcesz stworzyć klasy potomne, np. Dog i Cat, żeby mówiły nie "Jakiś dźwięk", tylko swoje prawdziwe kwestie ("Woof!" i "Meow!").
Można by w każdym potomku napisać swoją metodę Speak(), ale jeśli pracujesz z kolekcją typu Animal[], wywołanie animal.Speak() i tak odpali metodę Animal, a nie psa czy kota — bo C# domyślnie patrzy na typ zmiennej, a nie na faktyczny obiekt pod spodem.
Przykład na chłopski rozum:
Animal dog = new Dog();
dog.Speak(); // Ooo, co się wypisze?
Domyślnie — "Jakiś dźwięk", a nie "Woof!", nawet jeśli w Dog jest własna metoda Speak(). Co z tym zrobić? Trzeba zadeklarować metodę jako virtual (w bazowej klasie) i override (w pochodnej).
Co to jest metoda wirtualna?
virtual-metoda — to metoda, która jest zadeklarowana w bazowej klasie z modyfikatorem virtual i jest po to, żeby w potomkach można ją było nadpisać ( override).Gdy wywołasz taką metodę przez referencję do bazowej klasy, zostanie wywołana metoda faktycznego "żywego" obiektu — jak w życiu: jeśli oswoiłeś zwierzaka i to kot, to powie "Meow!", a nie "Jakiś dźwięk".
Metody wirtualne to podstawa polimorfizmu w C#.
2. Deklarowanie metod wirtualnych
Jak zadeklarować metodę wirtualną
W bazowej klasie deklarujemy metodę ze słowem kluczowym virtual:
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Jakiś dźwięk");
}
}
W potomku używamy słowa kluczowego override:
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
Przykład: pokaz polimorfizmu
// Na przykład w pliku Program.cs
// Bazowa klasa Animal z wirtualną metodą Speak
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Jakiś dźwięk");
}
}
// Klasa Dog, nadpisuje zachowanie Speak
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
// Klasa Cat, też nadpisuje Speak
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
public static class Program
{
public static void Main()
{
Animal[] animals = new Animal[]
{
new Animal(),
new Dog(),
new Cat()
};
foreach (Animal animal in animals)
{
animal.Speak(); // Wypisze: "Jakiś dźwięk", "Woof!", "Meow!"
}
}
}
Widzisz, jaka magia w metodzie wirtualnej? Nawet jeśli zmienna jest typu Animal, wywołuje się wersja metody z konkretnej implementacji.
3. Co się dzieje "pod maską": wirtualna tablica
Krótko o mechanizmie
Gdy deklarujesz metodę jako wirtualną, kompilator dorzuca ją do specjalnej "wirtualnej tablicy" metod dla każdego typu (prawie jak menu w stołówce dla każdego dania). Gdy wywołujesz taką metodę, C# patrzy nie na typ zmiennej, tylko na to, co faktycznie jest pod referencją — i "podstawia" tę metodę, która odpowiada faktycznemu typowi obiektu.
Można powiedzieć, że C# sam podkłada właściwy przepis zamiast uniwersalnego, jeśli klasa ma swoją wersję.
Analogicznie: umowa z możliwością "dopisać punkt drobnym drukiem"
Wyobraź sobie, że masz umowę najmu (bazowa klasa), i jest tam napisane, że najemca płaci domyślnie stałą kwotę co miesiąc (metoda PayRent()). Ale ty — cwany właściciel — pozwalasz w załącznikach do umowy wpisać specjalne warunki (metoda wirtualna). Jeśli któryś z najemców (potomek) skorzysta z tego prawa, napisze swój override i teraz opłata jest pobierana po swojemu.
4. Kiedy warto używać metod wirtualnych?
- Jeśli metoda w bazowej klasie powinna działać typowo dla większości potomków, ale niektóre mogą potrzebować własnego zachowania.
- Kiedy budujesz hierarchię klas, gdzie przewidujesz rozszerzanie lub zmianę części logiki.
- Kiedy tworzysz elastyczną architekturę, np. dla logiki biznesowej, gdzie różne typy operacji mogą wymagać indywidualnego zachowania.
W realnych projektach metody wirtualne to podstawa wzorców Metoda szablonowa (Template Method), Strategia (Strategy), no i w ogóle połowy OOP.
Porównanie ze zwykłymi metodami
| Zwykła metoda | Metoda wirtualna | |
|---|---|---|
| Można nadpisywać | Nie | Tak (override) |
| Jak się wywołuje | Po typie zmiennej | Po "prawdziwym" typie obiektu |
| Polimorfizm | Nie | Tak |
Najczęściej zadawane pytania
- Czy można zrobić konstruktor wirtualnym?
Nie! Konstruktory nigdy nie są wirtualne — zawsze działają ściśle według typu obiektu. - Czy metody wirtualne mogą być statyczne?
Nie, tylko metody instancyjne mogą być wirtualne. Statyczne nie biorą udziału w polimorfizmie, bo nie ma obiektu — a kto miałby nadpisywać? - Czy pola mogą być wirtualne?
Nie, tylko metody, właściwości i zdarzenia.
5. Praktyka: rozwijamy aplikację "Świat zwierząt"
W ramach naszej naukowej apki już zrobiłeś prostą hierarchię zwierzaków. Dodajmy im nowe metody wirtualne, żeby różne zwierzaki mogły jeść po swojemu!
Przykład kodu z komentarzami
// W bazowej klasie ustawiamy wirtualną metodę Eat
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Jakiś dźwięk");
}
public virtual void Eat()
{
Console.WriteLine("Je zwykłe jedzenie.");
}
}
// W Dog nadpisujemy Eat
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public override void Eat() => Console.WriteLine("Je psie jedzenie.");
}
// W Cat też swoje zachowanie
public class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow!");
public override void Eat() => Console.WriteLine("Je ryby.");
}
public static class Program
{
public static void Main()
{
Animal[] animals = new Animal[]
{
new Animal(),
new Dog(),
new Cat()
};
foreach (var animal in animals)
{
animal.Speak();
animal.Eat();
Console.WriteLine("---");
}
}
}
No i gotowe, teraz każdy kotek i piesek je po swojemu! A bazowy Animal dalej wcina coś dziwnego, jak prawdziwy default.
6. Typowe błędy i niuanse
- Można zapomnieć override w potomku — i wtedy metoda bazowej klasy zostaje na miejscu.
- Albo odwrotnie, przypadkiem zapomnieć virtual w bazowej klasie, wtedy w potomku nie da się napisać override.
Nie da się napisać override bez virtual
Kompilator C# nie pozwoli nadpisać metody, która nie była zadeklarowana jako wirtualna (albo abstrakcyjna) w bazowej klasie.
Po co jest override
override jasno mówi kompilatorowi i czytelnikowi kodu: "Nie piszę po prostu nowej metody — świadomie zmieniam to, co jest w bazowej klasie". To zapobiega błędom, gdy chcesz "nadpisać", ale przypadkiem piszesz nową metodę o podobnej sygnaturze.
Jeśli zapomniałeś override i napisałeś nową metodę
public class Dog : Animal
{
public void Speak()
{
Console.WriteLine("Woof!");
}
}
Ale to nie jest nadpisanie! Gdy odwołasz się do Dog przez zmienną typu Animal, wywoła się metoda zwierzaka ("Jakiś dźwięk"), a nie psa. O, te niespodzianki z typami!
GO TO FULL VERSION