CodeGym /Kursy /C# SELF /Błędy przy dziedziczeniu i nadpisywaniu

Błędy przy dziedziczeniu i nadpisywaniu

C# SELF
Poziom 25 , Lekcja 1
Dostępny

1. Wprowadzenie

Kiedy początkujący słyszy słowo dziedziczenie, może mu się wydawać, że to proste: bierzesz jakąś istniejącą klasę, trochę ją rozszerzasz albo zmieniasz — i gotowe! W rzeczywistości jest sporo niuansów. Błędy przy dziedziczeniu mogą objawiać się na różne sposoby: złe sygnatury, zapomniane kluczowe słowa, kiepski design hierarchii — wszystko to prowadzi do trudnych bugów.

Czasem błędy wychodzą od razu: kod się nie kompiluje. Innym razem — bugi przypominają o sobie dopiero w runtime, kiedy twój nowy SuperMegaLogger wypisuje zupełnie coś innego niż oczekują użytkownicy, albo w ogóle nic nie wypisuje. Smutek, rozczarowanie, długie godziny debugowania — znasz to uczucie?

Chodźmy razem przez najczęstsze błędy związane z dziedziczeniem i nadpisywaniem metod i "uzdrawiajmy" nasz kod po drodze.

2. Zapomniane virtual: dlaczego nie można nadpisać metody?

Problem

W C# można nadpisać (override) tylko te metody, które w klasie bazowej są oznaczone słowem virtual, albo jako abstract, albo jako override samego siebie (w łańcuchu dziedziczenia). Jeśli metoda nie ma tego słowa, próba napisania w klasie pochodnej override wywoła błąd kompilacji.


class Animal
{
    public void Speak()
    {
        Console.WriteLine("Zwierzę coś mówi.");
    }
}

class Cat : Animal
{
    // Błąd kompilacji! Metoda w klasie bazowej nie jest virtual, abstract ani override.
    public override void Speak()
    {
        Console.WriteLine("Miau!");
    }
}

Błąd będzie mniej więcej taki: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override".

Jak nie wpaść w tę pułapkę?

Żeby nadpisywać metody, koniecznie oznaczaj je w klasie bazowej jako virtual. Przy okazji, jeśli projektujesz klasy do rozszerzania, zawsze przemyśl, które metody mogą być przydatne do nadpisania.


class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Zwierzę wydaje dźwięk.");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Miau!");
    }
}

Po co to wszystko?
Oznaczając metodę jako virtual, jawnie pozwalasz potomkom zmieniać jej zachowanie, a kompilator staje się twoim sojusznikiem — nie pozwoli przypadkowo nadpisać "nie tej" metody.

3. Błąd z kluczowym słowem override i new

Czasem pojawia się odwrotny problem: autor klasy pochodnej chce "nadpisać" metodę, ale w klasie bazowej nie jest ona virtual. Kompilator nie pozwoli użyć override, ale pozwoli użyć new. Ale to już zupełnie inne zachowanie!


class Dog : Animal
{
    // To nie jest override, tylko ukrycie (hiding) metody z klasy bazowej.
    public new void Speak()
    {
        Console.WriteLine("Hau!");
    }
}

Jeśli wywołasz taką metodę przez referencję typu Dog — wszystko gra:


Dog dog = new Dog();
dog.Speak(); // "Hau!"

Ale jeśli podejdziesz do obiektu przez referencję typu bazowego, wywoła się metoda bazowa:


Animal dog2 = new Dog();
dog2.Speak(); // "Zwierzę wydaje dźwięk."

Wyjaśnienie:
Kluczowe słowo new NIE nadpisuje metody, tylko ją ukrywa. To się nazywa "hiding". Takie podejście może mylić, gdy polimorficzne zachowanie nie działa tak, jak programista oczekuje.

Jak uniknąć pułapek z new vs override?

Jeśli chcesz klasyczne nadpisanie z obsługą polimorfizmu — używaj virtual/override. Jeśli dodajesz zupełnie nową funkcjonalność (albo świadomie chcesz ukryć zachowanie metody bazowej, ale bądź ostrożny!), to użyj new.

Sytuacja Kluczowe słowo Czy polimorfizm działa? Zachowanie przy wywołaniu przez typ bazowy
Zmienić zachowanie override Tak Wywołuje się metoda klasy pochodnej
Ukryć/zastąpić metodę new Nie Wywołuje się metoda klasy bazowej

4. Niepasujące sygnatury metod

Początkujący, a czasem i doświadczeni programiści, mylą się, zmieniając typy parametrów lub typ zwracany w metodzie potomnej. Na przykład, jeśli metoda bazowa jest zadeklarowana jako public virtual void Print(string message), a w klasie pochodnej "nadpisują" public override void Print(object message), to już NIE jest override, tylko nowe zdefiniowanie metody.


class Printer
{
    public virtual void Print(string msg)
    {
        Console.WriteLine("Drukarka bazowa: " + msg);
    }
}

class SmartPrinter : Printer
{
    // Błąd kompilacji! Sygnatura nie pasuje do metody bazowej.
    public override void Print(object msg)
    {
        Console.WriteLine("Smart drukarka: " + msg);
    }
}

Podpowiedź:
Muszą się zgadzać i nazwa metody, i typ zwracany, i parametry (typy, ilość i kolejność).

Jeśli przypadkiem zmienisz typ któregoś z parametrów albo zrobisz literówkę w nazwie — kompilator ostrzeże.

5. Niezgodność modyfikatorów dostępu

Jeszcze jedna mina, na którą często wpadają początkujący — modyfikatory dostępu. Metoda potomna nie może być bardziej zamknięta niż bazowa. Przykład złego kodu:


public class Vehicle
{
    public virtual void StartEngine() { /* ... */ }
}

public class Car : Vehicle
{
    // Błąd! Modyfikator 'private' jest bardziej restrykcyjny niż 'public' w bazowej metodzie.
    private override void StartEngine() { /* ... */ }
}

Co robić?
Modyfikator dostępu w metodzie potomnej musi być taki sam lub bardziej otwarty niż w metodzie bazowej. W większości przypadków public albo protected.

6. Zapomniana/zbędna metoda abstrakcyjna

Jeśli w klasie bazowej metoda jest zadeklarowana jako abstract, to w klasie pochodnej MUSI być jej override, inaczej klasa też staje się abstrakcyjna (i nie można jej utworzyć).


abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    // Błąd! Niezaimplementowana metoda abstrakcyjna Area()
}

Rozwiązanie:
Trzeba zaimplementować tę metodę:


class Circle : Shape
{
    public override double Area()
    {
        return 3.14 * 2 * 2; // Tak mniej więcej...
    }
}

7. Wywołanie bazowej implementacji: base.Method()

Czasem trzeba nie całkiem zastąpić implementację metody, tylko coś do niej dodać. Wtedy często się zapomina (albo nie wie), że w metodzie potomnej można wywołać implementację klasy bazowej przez słowo base.


class Logger
{
    public virtual void Log(string msg)
    {
        Console.WriteLine("Bazowy log: " + msg);
    }
}

class FancyLogger : Logger
{
    public override void Log(string msg)
    {
        // Można dodać "bajery" i wywołać bazową metodę:
        Console.WriteLine("[FANCY] " + msg);
        base.Log(msg);
    }
}

Wyjaśnienie:
Jeśli nie wywołasz base.Log(msg), logika z klasy bazowej zostanie całkiem utracona.

8. Wywołanie bazowego konstruktora (base)

Jeśli klasa bazowa wymaga obowiązkowych parametrów w swoim konstruktorze, klasa pochodna musi jawnie wywołać odpowiedni konstruktor przez słowo base.


class Engine
{
    public Engine(int cylinders)
    {
        Console.WriteLine("Silnik z cylindrami: " + cylinders);
    }
}

class RaceEngine : Engine
{
    // Błąd kompilacji! Brak domyślnego konstruktora w Engine.
    public RaceEngine() { }
}

// Poprawiona wersja:
class RaceEngine2 : Engine
{
    public RaceEngine2() : base(8) // Jawnie wywołujemy bazowy konstruktor
    {
        Console.WriteLine("Wyścigowy silnik gotowy!");
    }
}

9. "Zapomniane" oznaczenie metod jako sealed

Czasem trzeba zabronić dalszego nadpisywania metody w klasach pochodnych — do tego w C# jest słowo sealed w połączeniu z override. Jeśli go nie użyjesz, ktoś może nadpisać twoją metodę, być może psując twoje założenia lub logikę.


class Hero
{
    public virtual void Attack() => Console.WriteLine("Bohater atakuje!");
}
class Warrior : Hero
{
    public sealed override void Attack() => Console.WriteLine("Wojownik zadaje cios!");
}
class Mutant : Warrior
{
    // Błąd! Metoda Attack oznaczona jako sealed wyżej.
    // public override void Attack() { ... }
}

Wyjaśnienie:
W ten sposób "zapieczętowujesz" implementację na tym poziomie i niższe klasy nie mogą jej zmienić.

10. Zbędne lub błędne przeciążenia zamiast nadpisania

Czasem programiści mylą przeciążenie metody (overloading) z nadpisaniem (overriding). Przeciążenie — to zdefiniowanie metody o tej samej nazwie, ale z inną sygnaturą (np. inna liczba parametrów), i nie ma to nic wspólnego z polimorfizmem.


class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("Zwierzę je.");
    }
}

class Panda : Animal
{
    // To NIE jest override! Po prostu nowa przeciążona metoda.
    public void Eat(string what)
    {
        Console.WriteLine("Panda je: " + what);
    }
}

...

Animal a = new Panda();
a.Eat(); // Jeśli metoda jest nadpisana — wywoła się implementacja z klasy pochodnej. Jeśli nie — użyje się bazowa
// a.Eat("bambus"); // Błąd kompilacji: taka metoda nie istnieje w Animal

Żeby dodać polimorficzne zachowanie, trzeba właśnie nadpisywać, a nie tylko przeciążać.

11. Formalne i nieformalne błędy projektowe

Kiedy klasy są źle zaprojektowane, listy dziedziczenia robią się zagmatwane:

  • wewnętrzna "karuzela" override'ów bez sensownego użycia base.,
  • niekonsekwentne użycie modyfikatorów,
  • zbyt głębokie lub nieoczywiste hierarchie,
  • metody, których część logiki jest "rozmazana" między poziomami dziedziczenia,
  • brak komentarzy do virtual/abstract metod,
  • nieoczywiste efekty uboczne (np. wywołanie metody virtual w konstruktorze klasy bazowej, gdy pochodna nie jest jeszcze do końca zainicjalizowana).

Rada:
Jeśli czujesz, że się pogubiłeś — nie żałuj czasu, narysuj sobie na kartce (stary, ale skuteczny sposób!) hierarchię i podpisz, która metoda gdzie jest, kto ją nadpisuje, co wywołuje i gdzie powinno być wywołane base..
W prawdziwych projektach mega ważne jest też w dokumentacji zaznaczyć, czego się oczekuje od nadpisania danej metody.

2
Zadanie
C# SELF, poziom 25, lekcja 1
Niedostępne
Błąd braku `virtual` przy dziedziczeniu metody
Błąd braku `virtual` przy dziedziczeniu metody
2
Zadanie
C# SELF, poziom 25, lekcja 1
Niedostępne
Błąd ukrywania metody z użyciem `new`
Błąd ukrywania metody z użyciem `new`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION