Підрозділ 4.5
Відмінність перевизначення та приховування методів
Пояснює різницю між перевизначенням і приховуванням методів, роботу VMT і вибір реалізації під час виконання.
4.5. Відмінність перевизначення та приховування методів
У попередніх розділах ми розглянули два способи змінити поведінку методу в похідному класі: перевизначення (virtual + override) і приховування (new). Зовні вони схожі — в обох випадках похідний клас має метод з тим самим іменем, що й базовий. Але поводяться вони принципово по-різному, і розуміння цієї різниці є критичним для написання коректного поліморфного коду.
Перевизначення: пізнє зв'язування
При перевизначенні через virtual + override рішення про те, яку реалізацію методу викликати, приймається під час виконання програми на основі реального типу об'єкта. Цей механізм називається пізнім зв'язуванням (late binding або dynamic dispatch).
Змінна p оголошена як Person, але реальний об'єкт у пам'яті — Doctor. При виклику p.Print() середовище виконання перевіряє реальний тип об'єкта і знаходить реалізацію Doctor.Print().
Таблиця віртуальних методів (VMT)
Для реалізації пізнього зв'язування компілятор C# формує для кожного класу таблицю віртуальних методів (Virtual Method Table, VMT). У цій таблиці зберігаються адреси реалізацій усіх віртуальних методів класу. Коли створюється об'єкт, він отримує посилання на VMT свого реального класу.
При виклику p.Print():
- Середовище виконання бере посилання на VMT з об'єкта — це VMT класу
Doctor - У VMT
Doctorзнаходить адресуDoctor.Print() - Виконує
Doctor.Print()
Саме тому тип змінної не має значення — вирішує VMT об'єкта. Варто врахувати, що через необхідність звернення до VMT виклик віртуального методу є трохи повільнішим порівняно зі звичайним. Проте у переважній більшості задач ця різниця непомітна.
Приховування: раннє зв'язування
При приховуванні через new рішення приймається під час компіляції на основі типу змінної. Це раннє зв'язування (early binding або static dispatch). Похідний клас не замінює метод у VMT — він просто визначає новий метод, який існує паралельно:
Той самий об'єкт Doctor — але залежно від типу змінної викликається різна реалізація. Через Person p — Person.Print(), через Doctor d — Doctor.Print(). Компілятор прив'язує виклик до методу ще при компіляції, дивлячись лише на тип змінної.
Порівняння на одному прикладі
Щоб побачити різницю повністю, розглянемо масив Person[], де зберігаються різні об'єкти:
Якби замість override використовувалось new — всі три виклики у циклі виводили б Person.Print(), оскільки змінна циклу p має тип Person.

Підсумок
override (перевизначення) |
new (приховування) |
|
|---|---|---|
| Зв'язування | Пізнє (runtime) | Раннє (compile time) |
| Визначає виклик | Реальний тип об'єкта | Тип змінної |
| Поведінка | Поліморфна | Залежить від змінної |
| Потребує | virtual у базовому |
Будь-який метод |
| VMT | Замінює запис | Не змінює VMT |
Загальне правило: якщо потрібна поліморфна поведінка — virtual + override. Якщо потрібно локально перевизначити невіртуальний метод без поліморфізму — new. Змішування цих підходів без чіткого розуміння є одним із найпоширеніших джерел прихованих помилок у C#.