JavaRush /Курси /C# SELF /Помилки під час наслідування та перевизначення

Помилки під час наслідування та перевизначення

C# SELF
Рівень 25 , Лекція 1
Відкрита

1. Вступ

Коли ви вперше чуєте слово наслідування, може здатися, що все просто: достатньо взяти наявний клас, трохи його розширити чи змінити — і готово. Проте нюансів чимало. Помилки під час наслідування можуть виявлятися по‑різному: неправильні сигнатури, забуті ключові слова, невдалий дизайн ієрархії — усе це призводить до складних багів.

У деяких випадках помилки видно відразу: код не компілюється. В інших — помилки даються взнаки лише під час виконання програми, коли ваш новий SuperMegaLogger пише зовсім не те, чого очікують користувачі, або взагалі нічого не пише. Розчарування, довгі години налагодження — знайоме відчуття?

Пройдімося разом найчастішими помилками, повʼязаними з наслідуванням і перевизначенням методів, і будемо «лікувати» наш код у процесі.

2. Забутий virtual: чому метод не можна перевизначити?

Проблема

У C# перевизначити (override) можна лише ті методи, які в базовому класі оголошені з ключовим словом virtual, або як abstract, або як override у ланцюжку наслідування. Якщо метод не має цього слова, спроба написати в похідному класі override спричинить помилку компіляції.


class Animal
{
    public void Speak()
    {
        Console.WriteLine("Тварина щось каже.");
    }
}

class Cat : Animal
{
    // Помилка компіляції! Метод у базовому класі не virtual, не abstract і не override.
    public override void Speak()
    {
        Console.WriteLine("Мяу!");
    }
}

Помилка буде приблизно така: «'Cat.Speak()': не можна override успадкований член 'Animal.Speak()', бо він не позначений як virtual, abstract або override».

Як цього уникнути?

Щоб перевизначати методи, обовʼязково оголошуйте такі методи в базовому класі як virtual. До речі, якщо ви проєктуєте класи для розширення, завжди зважайте, які методи можуть бути корисні для перевизначення.


class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Тварина видає звук.");
    }
}

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

Навіщо це потрібно?
Оголошуючи метод як virtual, ви явно дозволяєте нащадкам змінювати його поведінку, а компілятор стає вашим союзником — він не дасть випадково перевизначити «не той» метод.

3. Помилка з ключовими словами override і new

Іноді виникає зворотна проблема: автор похідного класу хоче «перевизначити» метод, але в базовому класі він не virtual. Тоді компілятор не дозволить використати override, але дозволить new. Проте це вже зовсім інша поведінка.


class Dog : Animal
{
    // Це не override, а приховування (hiding) методу базового класу.
    public new void Speak()
    {
        Console.WriteLine("Гав!");
    }
}

Якщо викликати такий метод через посилання типу Dog — усе гаразд:


Dog dog = new Dog();
dog.Speak(); // "Гав!"

Але якщо звернутися до обʼєкта через посилання базового типу, викличеться базовий метод:


Animal dog2 = new Dog();
dog2.Speak(); // "Тварина видає звук."

Пояснення:
Ключове слово new не перевизначає метод, а ховає батьківський. Це називається «hiding». Такий підхід може заплутати, коли поліморфна поведінка працює не так, як ви очікуєте.

Як уникнути пасток із new і override?

Якщо ви хочете класичне перевизначення з підтримкою поліморфізму — використовуйте virtual/override. Якщо додаєте зовсім нову функціональність (або свідомо хочете приховати поведінку базового методу — будьте обережні!), тоді використовуйте new.

Ситуація Ключове слово Поліморфізм працює? Поведінка під час звернення через базовий тип
Змінити поведінку override Так Викликається метод похідного класу
Приховати/замінити метод new Ні Викликається метод базового класу

4. Несумісні сигнатури методів

Новачки, а іноді й досвідчені розробники, помиляються, змінюючи типи параметрів або тип повернення у методі нащадка. Наприклад, якщо базовий метод оголошено як public virtual void Print(string message), а в похідному класі «перевизначають» public override void Print(object message), це вже не override, а нове визначення методу.


class Printer
{
    public virtual void Print(string msg)
    {
        Console.WriteLine("Базовий принтер: " + msg);
    }
}

class SmartPrinter : Printer
{
    // Помилка компіляції! Сигнатура не збігається з базовим методом.
    public override void Print(object msg)
    {
        Console.WriteLine("Розумний принтер: " + msg);
    }
}

Підказка:
Мають збігатися і імʼя методу, і тип повернення, і параметри (за типами, кількістю та порядком).

Якщо ви випадково змінили тип одного з параметрів або помилилися в назві — компілятор попередить.

5. Невідповідність модифікаторів доступу

Ще одна поширена помилка — модифікатори доступу. Похідний метод не може мати суворіший рівень доступу, ніж базовий. Антиприклад:


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

public class Car : Vehicle
{
    // Помилка! Модифікатор 'private' суворіший, ніж 'public' у базового методу.
    private override void StartEngine() { /* ... */ }
}

Що робити?
Модифікатор доступу в методі нащадка має бути таким самим або більш відкритим, ніж у базового методу. У більшості випадків — public або protected.

6. Забутий або зайвий абстрактний метод

Якщо в базовому класі метод оголошено як abstract, то в похідному класі обовʼязково має бути його override, інакше клас також стає абстрактним (і його не можна створити).


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

class Circle : Shape
{
    // Помилка! Не реалізовано абстрактний метод Area()
}

Рішення:
Потрібно реалізувати цей метод:


class Circle : Shape
{
    public override double Area()
    {
        return 3.14 * 2 * 2; // Приблизно...
    }
}

7. Виклик базової реалізації: base.Method()

Іноді треба не повністю замінити реалізацію методу, а щось до неї додати. У такому випадку часто забувають (або не знають), що в методі нащадка можна викликати реалізацію базового класу через ключове слово base.


class Logger
{
    public virtual void Log(string msg)
    {
        Console.WriteLine("Базовий лог: " + msg);
    }
}

class FancyLogger : Logger
{
    public override void Log(string msg)
    {
        // Можна додати додаткову логіку і викликати базовий метод:
        Console.WriteLine("[FANCY] " + msg);
        base.Log(msg);
    }
}

Пояснення:
Якщо не викликати base.Log(msg), логіка, закладена в базовому класі, буде повністю втрачена.

8. Виклик базового конструктора (base)

Якщо базовий клас вимагає обовʼязкових параметрів у своєму конструкторі, похідний клас має явно викликати відповідний конструктор через ключове слово base.


class Engine
{
    public Engine(int cylinders)
    {
        Console.WriteLine("Двигун із циліндрами: " + cylinders);
    }
}

class RaceEngine : Engine
{
    // Помилка компіляції! Немає конструктора за замовчуванням у Engine.
    public RaceEngine() { }
}

// Виправлений варіант:
class RaceEngine2 : Engine
{
    public RaceEngine2() : base(8) // Викликаємо базовий конструктор явно
    {
        Console.WriteLine("Гоночний двигун готовий!");
    }
}

9. «Забута» позначка методів як sealed

Іноді потрібно заборонити подальше перевизначення методу в похідних класах — для цього в C# є ключове слово sealed разом із override. Якщо ви його не використовуєте, хтось може перевизначити ваш метод, можливо, зламавши ваші очікування чи логіку.


class Hero
{
    public virtual void Attack() => Console.WriteLine("Герой атакує!");
}
class Warrior : Hero
{
    public sealed override void Attack() => Console.WriteLine("Воїн завдає удару!");
}
class Mutant : Warrior
{
    // Помилка! Метод Attack позначено як sealed вище.
    // public override void Attack() { ... }
}

Пояснення:
Таким чином ви «запечатуєте» реалізацію на цьому рівні, і нижчі класи не зможуть її змінити.

10. Зайві або неправильні перевантаження замість перевизначення

У деяких випадках програмісти плутають перевантаження методу (overloading) і перевизначення (overriding). Перевантаження — це визначення методу з тим самим імʼям, але іншою сигнатурою (наприклад, інша кількість параметрів), і до поліморфізму це не має стосунку.


class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("Тварина їсть.");
    }
}

class Panda : Animal
{
    // Це НЕ override! Просто новий перевантажений метод.
    public void Eat(string what)
    {
        Console.WriteLine("Панда їсть: " + what);
    }
}

...

Animal a = new Panda();
a.Eat(); // Якщо метод перевизначено — буде викликана реалізація з похідного класу. Якщо ні — буде використано базову
// a.Eat("бамбук"); // Помилка компіляції: такого методу нема у Animal

Щоб додати поліморфну поведінку, потрібно саме перевизначати, а не просто перевантажувати.

11. Формальні та неформальні помилки під час проєктування

Коли класи спроєктовані невдало, списки наслідувань стають заплутаними:

  • внутрішня «карусель» override-ів без належного використання base.,
  • непослідовне використання модифікаторів,
  • занадто глибокі або неочевидні ієрархії,
  • методи, у яких частина логіки «розмазана» між рівнями наслідування,
  • відсутність коментарів або документації до virtual/abstract‑методів,
  • неочевидні побічні ефекти (наприклад, виклик virtual‑методу в конструкторі базового класу, коли похідний ще не до кінця проініціалізований).

Порада:
Якщо відчуваєте, що заплуталися, — намалюйте на аркуші ієрархію та підпишіть, який метод де перебуває, хто його перевизначає, що викликає і де має бути виклик base..
У реальних проєктах дуже важливо також у документації відмічати, що очікується від перевизначення того чи іншого методу.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ