1. Вступ
Уявімо, що у нас є застосунок для обліку тварин у зоопарку (той самий приклад, який ми почали розвивати на попередніх лекціях). Ми створили базовий клас Animal з методом MakeSound, щоб усі тварини могли «видавати» якийсь звук.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Тварина видає звук.");
}
}
Та раптом зʼясовується, що у лева та папуги звуки зовсім різні. Універсальне «Тварина видає звук» уже не підходить. Саме тут у пригоді стає перевизначення методів. Вам потрібно, щоб кожен похідний клас звучав по‑своєму!
2. Синтаксис: як працює override
Основні правила
- Щоб перевизначити метод, він має бути позначений у базовому класі як virtual (або abstract).
- У похідному класі використовуємо ключове слово override перед методом із такою самою сигнатурою.
Приклад: леви та папуги
public class Lion : Animal
{
public override void MakeSound()
{
Console.WriteLine("Ррррр!");
}
}
public class Parrot : Animal
{
public override void MakeSound()
{
Console.WriteLine("Папуга дурень!");
}
}
Тепер, якщо ви створите список тварин і викличете для кожного MakeSound, кожен «говоритиме» по‑своєму:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Animal()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
// Виведення:
// Ррррр!
// Папуга дурень!
// Тварина видає звук.
3. Віртуальна таблиця викликів (v-table)
Коли ви використовуєте virtual і override, компілятор генерує для класу спеціальну «віртуальну таблицю методів» (v-table).
При виклику методу через посилання на базовий клас CLR звертається до таблиці: чи не перевизначено цей метод у похідному класі? Якщо так — викликається варіант із похідного класу.
Це й є «магія» пізнього зв’язування (late binding), або динамічного поліморфізму.
4. З’єднуємо разом: продовжуємо розвивати наш застосунок
Додаймо ще один клас:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав-гав!");
}
}
Скористаймося цим у нашій мініпрограмі обліку тварин:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Dog()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
Тепер розширюваність і підтримуваність коду зросли у рази, а додавати нових тварин — одне задоволення!
5. Перевизначення властивостей і override з поверненим значенням
Методи — не єдине, що можна перевизначати. Ви також можете перевизначати віртуальні властивості та індексатори.
public class Animal
{
public virtual string Name { get; set; } = "Тварина";
}
public class Lion : Animal
{
public override string Name { get; set; } = "Лев";
}
Це особливо зручно, коли треба уточнити інформацію для конкретного виду.
6. Базова реалізація за допомогою base
Іноді потрібно розширити поведінку базового методу, а не повністю його замінити.
Для цього викликаємо реалізацію базового методу через ключове слово base усередині перевизначеного методу.
public class Parrot : Animal
{
public override void MakeSound()
{
base.MakeSound(); // "Тварина видає звук."
Console.WriteLine("Папуга дурень!");
}
}
У цьому випадку папуга спочатку видасть загальний «зоопарковий» звук, а потім скаже своє фірмове «Папуга дурень!».
7. Практична користь: де це застосовують
Розуміння механізму override необхідне практично у будь-якому серйозному проєкті на C#, де активно використовують ООП.
- Моделі тварин у нашому зоопарку? Вже робили.
- Створення власних елементів керування для UI: Ви перевизначаєте стандартні візуальні методи, додаючи власну логіку.
- Гнучка бізнес-логіка: Дозволяє будувати «скелет» поведінки в базових класах, а конкретику реалізовувати у похідних класах.
- Тестування та моки: Легко «підміняти» методи через нащадків для юніт‑тестів.
- Плагіни та розширення: Інтерфейс або абстрактний базовий клас, багато реалізацій — і все працює завдяки правильному перевизначенню.
8. sealed override і віртуальні методи у ланцюжках
Якщо ви не хочете, щоб хтось далі в ієрархії перевизначав ваш перевизначений метод, можна застосувати 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() {} // Помилка — не можна перевизначити!
}
Це дозволяє «закрити» ланцюжок перевизначень там, де це критично для коректної роботи системи.
9. Новий метод (ключове слово new)
Буває, що потрібно оголосити у похідному класі метод із такою самою сигнатурою, але базовий не virtual.
У такому випадку можна використати ключове слово new — але це не поліморфізм, а радше «приховування»:
public class Animal
{
public void MakeSound()
{
Console.WriteLine("Я тварина!");
}
}
public class Cat : Animal
{
public new void MakeSound()
{
Console.WriteLine("Я котик!");
}
}
Animal animal = new Cat();
animal.MakeSound(); // "Я тварина!"
Cat cat = new Cat();
cat.MakeSound(); // "Я котик!"
Тут усе визначає тип змінної в момент виклику! Тому для справжнього динамічного поліморфізму завжди використовуйте комбінацію virtual + override.
10. Типові помилки при перевизначенні методів
Помилка № 1: спроба перевизначити метод, який не був оголошений як virtual, abstract або override.
У C# не можна перевизначати звичайні методи. Якщо метод у базовому класі не позначений спеціальним модифікатором (virtual, abstract або override), спроба написати override у похідному класі спричинить помилку компіляції.
Помилка № 2: невідповідність сигнатур методу.
Щоб метод був справді перевизначений, його імʼя, тип, що повертається, і параметри мають повністю збігатися з методом у базовому класі. Найменша розбіжність (наприклад, інший тип параметра) призведе до створення нового методу, а не до перевизначення.
Помилка № 3: забули модифікатор override.
Якщо ви визначили метод із такою самою сигнатурою, як у базовому класі, але не вказали override, компілятор не вважатиме це перевизначенням. Це називається приховуванням методу (буде розглянуто окремо). У такому випадку виклик методу через змінну базового типу призведе до неочікуваного результату — буде викликано базовий метод, а не ваш.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ