CodeGym /Kursy /C# SELF /Nadpisywanie metod (override) w C#

Nadpisywanie metod (override) w C#

C# SELF
Poziom 20 , Lekcja 3
Dostępny

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.");
    }
}
        
Klasa bazowa z wirtualną metodą

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.
Ważne: C# wywoła dokładnie tę implementację metody, która pasuje do rzeczywistego ( run-time) typu obiektu, nawet jeśli zmienna jest zadeklarowana jako Animal!

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.

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