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, компилятор не считает это переопределением. Это называется скрытие метода (будет рассмотрено отдельно). В таком случае вызов метода через переменную базового типа приведёт к неожиданному результату — будет вызван базовый метод, а не ваш.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ