Как работают виртуальные методы в C# и для чего они используются
Что проверяют
Понимаете ли вы, как вызов выбирается во время выполнения и чем override отличается от new. Следом обычно спрашивают разницу между virtual и abstract или что будет, если вызвать виртуальный метод из конструктора.
Как отвечать
Методы в C# по умолчанию невиртуальные, в отличие от Java. Метод с virtual наследник может переопределить через override, и тогда реализация выбирается по фактическому типу объекта, а не по типу переменной: Animal a = new Dog(); a.Speak() вызовет версию Dog.
Механизм: у каждого типа в CLR есть таблица методов со слотами под виртуальные методы. override кладёт в унаследованный слот свою реализацию, а вызов идёт косвенно через этот слот. Компилятор выдаёт callvirt даже для невиртуальных методов экземпляра, ради проверки на null.
Родственные модификаторы:
abstract: виртуальный метод без тела, переопределять обязательно, класс тоже абстрактный;sealed override: запрещает переопределять метод дальше по иерархии.sealedна классе ещё и помогает JIT убрать косвенный вызов;new: сокрытие, а не переопределение. Появляется новый метод с тем же именем, и через ссылку на базовый тип вызовется базовая версия;base.Speak(): вызов базовой реализации из переопределения.
Где используется: шаблонный метод и точки расширения фреймворка (ToString, Equals, GetHashCode, Dispose(bool)), ленивая загрузка в EF Core через прокси, которой нужны virtual-навигационные свойства, моки в Moq, которые подменяют только виртуальные члены классов.
Цена: косвенный вызов мешает встраиванию, но динамическая PGO (по умолчанию с .NET 8) позволяет JIT угадать частый тип и девиртуализировать вызов.
Где спотыкаются
Путают override и new. Компилятор предупреждает о сокрытии, а поведение через базовую ссылку отличается принципиально.
Вызывают виртуальный метод из конструктора базового класса. Выполнится переопределение наследника, хотя тело его конструктора ещё не отработало, и поля, которые там задаются, пока пустые.
Делают все методы virtual «на будущее». Каждый такой метод становится обязательством: базовый класс уже нельзя менять, не оглядываясь на все переопределения.