CodeGym /Kursy /C# SELF /Nadpisywanie metod (Method Overriding) w C#

Nadpisywanie metod (Method Overriding) w C#

C# SELF
Poziom 21 , Lekcja 2
Dostępny

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

Dog dziedziczy po Animal i może nadpisać MakeSound()

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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION