Что такое виртуальные методы в C# и как они работают?

Ответ

Виртуальные методы — это механизм поддержки полиморфизма в C#, позволяющий производному классу предоставить свою собственную реализацию метода, уже определенного в базовом классе. Выбор конкретной реализации происходит во время выполнения (позднее связывание) на основе фактического типа объекта.

Как это работает?

  1. В базовом классе метод помечается ключевым словом virtual.
  2. В производном классе метод переопределяется с помощью ключевого слова override.
  3. При вызове метода у ссылки типа базового класса, которая ссылается на объект производного класса, будет выполнена переопределенная версия.

Практический пример:

public class NotificationSender
{
    // Виртуальный метод с реализацией по умолчанию
    public virtual void Send(string message)
    {
        Console.WriteLine($"[Base Sender] Отправка базового уведомления: {message}");
    }

    // Невиртуальный метод. Его нельзя переопределить, можно только скрыть (new).
    public void Log(string action)
    {
        Console.WriteLine($"[Base Log] Действие: {action}");
    }
}

public class EmailSender : NotificationSender
{
    // Переопределение виртуального метода
    public override void Send(string message)
    {
        // Можно вызвать базовую реализацию, если нужно
        // base.Send(message);
        Console.WriteLine($"[Email] Отправлено на email: {message}");
    }
}

public class SmsSender : NotificationSender
{
    public override void Send(string message)
    {
        Console.WriteLine($"[SMS] Отправлено SMS: {message}");
    }

    // Скрытие (shadowing) метода базового класса, а не переопределение.
    public new void Log(string action)
    {
        Console.WriteLine($"[SMS Log] {action}");
    }
}

// Использование:
NotificationSender sender1 = new EmailSender();
NotificationSender sender2 = new SmsSender();
NotificationSender sender3 = new NotificationSender();

// Полиморфный вызов. Реализация определяется реальным типом объекта.
sender1.Send("Заказ создан"); // Вывод: [Email] Отправлено на email: Заказ создан
sender2.Send("Заказ создан"); // Вывод: [SMS] Отправлено SMS: Заказ создан
sender3.Send("Заказ создан"); // Вывод: [Base Sender] Отправка базового уведомления: Заказ создан

// Важно: для невиртуальных методов вызов определяется типом ссылки.
sender2.Log("test"); // Вывод: [Base Log] Действие: test (т.к. ссылка типа NotificationSender)
((SmsSender)sender2).Log("test"); // Вывод: [SMS Log] test
Ключевые отличия virtual/override от new: Аспект virtual / override new (скрытие)
Связывание Позднее (runtime) Раннее (compile-time)
Полиморфизм Поддерживает Не поддерживает
Вызов через ссылку базового класса Вызывается метод производного класса Вызывается метод базового класса

Зачем это нужно? Виртуальные методы — основа для реализации шаблонов проектирования (например, Template Method), создания расширяемых библиотек и фреймворков, где поведение базовых классов должно настраиваться наследниками.

Ответ 18+ 🔞

Давай разжую эту тему про виртуальные методы, чтобы было понятно даже тому, кто только вчера узнал, что C# — это не только решётка перед цифрами.

Представь себе, что у тебя есть базовый класс — этакий отец-основатель, у которого есть некая фишка, метод. Но он такой: «Я, конечно, могу сделать это по-своему, но если моим наследникам — твоим классам-сыночкам — не понравится, пусть делают как хотят». Вот чтобы это зафиксировать, он помечает свой метод словом virtual. Это как дать добро на вольную интерпретацию.

А теперь появляется его наследник, молодой и дерзкий. Он смотрит на метод отца и говорит: «Да ну нахуй, я сделаю это лучше и по-своему!». И чтобы все, включая компилятор, поняли, что это осознанное переопределение, а не случайное совпадение имён, он ставит ключевое слово override. И вот тут начинается магия полиморфизма, или, как я это называю, «объебономика времени выполнения».

Суть в чём: когда ты вызываешь метод у ссылки типа «папа» (NotificationSender), которая на самом деле указывает на объект типа «сын» (EmailSender), система смотрит не на то, КАКОЙ тип у ссылки, а на то, КАКОЙ тип у реального объекта в памяти. И если там есть override, она запускает именно его. Это и есть позднее связывание — решение принимается в рантайме, прямо во время работы программы. Красота, да?

NotificationSender sender = new EmailSender(); // Ссылка - папа, объект - сын.
sender.Send("Привет"); // Вызовется EmailSender.Send, потому что он override.

А теперь про new, он же скрытие. Это когда наследник такой хитрожопый: «Я вроде как сделаю метод с таким же именем, как у отца, но чтобы мой старик не догадался, что я его игнорирую». По факту, он просто создаёт СОВСЕМ НОВЫЙ метод, который никак не связан с родительским. И когда ты вызываешь его через ссылку типа «папа», компилятор, ещё на этапе сборки, говорит: «Ага, ссылка типа NotificationSender, значит, берём метод из NotificationSender». И всё. Никакой магии, никакого полиморфизма. Раннее связывание, тупо и в лоб.

NotificationSender sender = new SmsSender(); // Ссылка - папа, объект - сын с new-методом.
sender.Log("test"); // Вызовется NotificationSender.Log, потому что new — это не переопределение, а сокрытие.
((SmsSender)sender).Log("test"); // А вот так вызовется уже SmsSender.Log, потому что мы сказали компилятору смотреть на реальный тип.

Итоговая таблица, чтобы не ебал мозг:

Что сравниваем virtual / override new (скрытие)
Когда выбирается? В момент выполнения (runtime), смотря на РЕАЛЬНЫЙ тип объекта. В момент компиляции (compile-time), смотря на ТИП ССЫЛКИ.
Это полиморфизм? Да, ёпта! Это его суть. Нет, блядь. Это просто два разных метода с одним именем.
Вызов через папу Запустится метод сына (если он override). Запустится метод папы (потому что сын его просто скрыл).

Зачем это всё, спросишь? Да всё просто. Без virtual и override не было бы ни нормальных библиотек, ни фреймворков. Представь, ты пишешь игровой движок с классом Enemy. У него метод Update(). Ты хочешь, чтобы для каждого типа врага (зомби, пришелец, начальник) логика обновления была своя. Помечаешь метод virtual, а в классах Zombie и Alien переопределяешь его через override. И движок, вызывая enemy.Update(), даже не парится, кто там конкретно — он знает, что вызовется правильная реализация. Это и есть красота и мощь, а не хухры-мухры.