1. Вступ
Якщо вам стало трохи не по собі від слова перевизначення, не переймайтеся — це не страшно й навіть зручно! Перевизначення методу — це можливість підмінити поведінку методу базового класу власною у похідному класі. Завдяки цьому наш код стає гнучким, розширюваним і готовим до реального життя, де кожна тварина точно не хоче бути просто «якимось звуком».
Щоб дозволити перевизначити метод, у базовому класі метод позначають ключовим словом virtual. У похідному класі, щоб замінити реалізацію, використовують ключове слово override.
Приклад
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("Деякий універсальний звук тварини...");
}
}
А тепер — у класі собаки:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав-гав!");
}
}
- У базовому класі — virtual.
- У похідному класі — override.
- Сигнатура методу (імʼя, тип поверненого значення, параметри) має збігатися.
Візуальна схема
2. Демонстрація роботи
Давайте подивимося, як працює перевизначення на практиці. Створимо тварин і перевіримо звук:
Animal pet1 = new Animal { Name = "Безіменна тварина" };
Dog pet2 = new Dog { Name = "Барбос" };
pet1.MakeSound(); // Виведе: Деякий універсальний звук тварини...
pet2.MakeSound(); // Виведе: Гав-гав!
Тепер ускладнимо завдання:
Що, якщо ми збережемо собаку у змінну типу Animal?
Animal pet3 = new Dog { Name = "Шарик" };
pet3.MakeSound(); // ???
Як вважаєте, що станеться?
Відповідь: Виведеться «Гав-гав!»
Адже навіть якщо змінна має тип Animal, вона «посилається» на собаку, тож буде викликано перевизначену версію методу!
Ось вона — магія динамічного (або пізнього) зв’язування.
3. Використання ключового слова base при перевизначенні
Іноді потрібно не повністю замінити реалізацію методу, а розширити її — наприклад, додати щось своє, а потім виконати стару поведінку. Для цього використовують ключове слово base. Воно дозволяє викликати версію методу з базового класу.
public class Cat : Animal
{
public override void MakeSound()
{
base.MakeSound(); // виклик базової реалізації
Console.WriteLine("Мяу!");
}
}
Під час виклику цього методу спочатку буде виведено «Деякий універсальний звук тварини…», а потім «Мяу!».
4. Як працює вибір методу при перевизначенні
Щоб наочно зрозуміти, що відбувається «під капотом», уявіть собі таку таблицю (віртуальна диспетчеризація):
| Тип змінної | Тип об’єкта | Який метод буде викликано |
|---|---|---|
| Animal | Animal | Animal.MakeSound |
| Animal | Dog | Dog.MakeSound |
| Animal | Labrador | Labrador.MakeSound |
| Dog | Labrador | Labrador.MakeSound |
| Dog | Dog | Dog.MakeSound |
Головне правило:
Тип змінної важливий лише для компілятора, а під час виконання враховується тип фактичного об’єкта (те, що ми створили через new).
Цей механізм називається динамічним (або пізнім) зв’язуванням — саме на ньому ґрунтується поліморфізм (про це — у наступній лекції!).
Навіщо перевизначати методи
- У GUI-фреймворках: у вас є базовий клас вікна, і ви перевизначаєте методи для малювання конкретних елементів.
- У ігрових рушіях: є базовий клас Enemy, а похідні класи реалізують різні типи поведінки.
- У юніт-тестах: можна створювати «заглушки» (stubs, mocks) для методів.
Сучасні фреймворки .NET активно використовують цей механізм для подій, шаблонного коду, наслідування конфігурацій і навіть для серіалізації об’єктів (наприклад, через віртуальні властивості).
5. Ключове слово new при приховуванні методів
Ми вже з’ясували, що для перевизначення методів потрібен дует virtual/override. Але в C# є ще один модифікатор, пов’язаний із методами в ієрархії наслідування — це new.
Навіщо потрібен new?
new використовують, якщо у похідному класі оголошено метод з тією ж сигнатурою, що й у базовому класі, АЛЕ ви не хочете перевизначати віртуальний метод, а саме приховати (замаскувати) базовий метод.
- Це не перевизначення, а приховування.
- Такий метод викликається за типом змінної, а не за фактичним типом об’єкта (немає динамічного поліморфізму!).
- Компілятор попередить, якщо ви «випадково» приховали метод без ключового слова new.
Приклад: різниця між override і new
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Тварина видає якийсь звук...");
}
}
public class Dog : Animal
{
// Приховуємо метод базового класу (НЕ перевизначаємо)
public new void MakeSound()
{
Console.WriteLine("Це не override! Просто собачий метод.");
}
}
Тепер подивімося на поведінку:
Animal a = new Dog();
Dog d = new Dog();
a.MakeSound(); // "Тварина видає якийсь звук..."
d.MakeSound(); // "Це не override! Просто собачий метод."
- Якщо змінна типу Dog — буде викликано метод із Dog.
- Якщо змінна типу Animal, навіть якщо в ній збережено Dog — буде викликано метод Animal!
6. Зворотний зв’язок і особливості реалізації
На перших порах у програмуванні часто трапляється непорозуміння: метод «перевизначено», але чомусь він працює по-старому. Зазвичай причина проста: у базовому класі немає virtual, або в похідному метод оголошено з new, а не з override. Другий випадок особливо підступний — якщо викликати метод через змінну базового типу, буде викликано базову версію, а не перевизначену. Тож завжди стежте, щоб правильно використовувати ключові слова.
Окрім синтаксичних помилок, іноді початківці намагаються змінити тип, що повертається, під час перевизначення. Наприклад, зробити базову функцію з типом object, а в похідній — з типом string. Так робити не можна: сигнатура методу має повністю збігатися.
Таблиця порівняння: override vs new
| Особливість | override | new |
|---|---|---|
| Механізм | Перевизначає віртуальний метод | Приховує метод базового класу |
| Пізнє зв’язування | Так — працює через динамічний поліморфізм | Ні — працює за типом змінної |
| Вимагає, щоб у базовому класі був… | virtual, abstract або вже override | Ні |
| Рекомендується використовувати | Так, майже завжди | Тільки у виняткових випадках |
7. Робота перевизначених методів з ієрархіями
Уся ця історія з перевизначенням стає особливо цікавою, якщо в нас є довгі ланцюги наслідування:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Тварина щось робить...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав-гав!");
}
}
public class Labrador : Dog
{
public override void MakeSound()
{
Console.WriteLine("Я лабрадор: вау-вау!");
}
}
Що відбувається, якщо ми напишемо:
Animal pet = new Labrador();
pet.MakeSound(); // => "Я лабрадор: вау-вау!"
C# завжди обирає найглибшу реалізацію віртуального методу, що є в самому об’єкті.
8. Типові помилки при перевизначенні методів
Світ неідеальний, і студенти (та досвідчені розробники) іноді помиляються. Давайте одразу навчимося уникати найпопулярніших «граблів»:
1. Забули virtual у базовому класі
public class Animal
{
public void MakeSound() { ... } // Немає 'virtual'
}
public class Dog : Animal
{
// Помилка компіляції! Не можемо перевизначити.
public override void MakeSound()
{
Console.WriteLine("Гав!");
}
}
C# одразу скаже: «Dog.MakeSound()»: no suitable method found to override
2. Сигнатури не збігаються
Переконайтеся, що імʼя методу, тип, що повертається, і параметри збігаються:
public class Animal
{
public virtual void MakeSound() { ... }
}
public class Dog : Animal
{
// Помилка: сигнатура відрізняється (наприклад, додано параметр)
public override void MakeSound(string sound)
{
Console.WriteLine(sound);
}
}
3. Не використовуйте new замість override без потреби
Ключове слово new дозволяє приховувати метод базового класу, але це не перевизначення і не працює через динамічний поліморфізм. Це інший механізм, якого зазвичай слід уникати без вагомої причини.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ