Как реализовано наследование в C#
Что проверяют
Знание механики наследования в C#: одиночное наследование классов, множественная реализация интерфейсов, разница между virtual/override/new и понимание, где виртуальная диспетчеризация работает, а где выбор идёт по типу переменной.
Как отвечать
Класс наследует ровно один базовый класс и сколько угодно интерфейсов. Все типы в итоге происходят от System.Object. Структуры наследовать нельзя, sealed класс наследовать нельзя.
Ключевые слова:
baseдаёт доступ к членам базового класса и вызывается из конструктора производного;virtualразрешает переопределение,overrideпереопределяет;abstractна классе запрещает создание экземпляра, на методе требует реализации в наследнике;newскрывает член базового класса, а не переопределяет его;sealed overrideзакрывает дальнейшее переопределение вниз по иерархии.
public class Animal
{
public virtual void Speak() => Console.WriteLine("Animal");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Dog");
}
Animal a = new Dog();
a.Speak(); // Dog: вызов идёт по фактическому типуЕсли в Dog написать public new void Speak(), тот же код напечатает Animal: при скрытии метод выбирается по типу переменной, а не объекта. Это и есть демонстрация полиморфизма и его отсутствия в одном примере.
Ещё несколько вещей, которые стоит проговорить:
- Конструкторы не наследуются, и базовый конструктор отрабатывает раньше производного.
- Модификаторы доступа:
protectedвиден наследникам,protected internalнаследникам плюс всей сборке,private protectedнаследникам внутри той же сборки. - Множественного наследования классов нет осознанно, чтобы не разбирать ромбовидную неоднозначность. Интерфейсы её решают, а с C# 8 у них есть реализации по умолчанию, но состояния (полей) они по-прежнему не несут.
- Записи (
record) наследование поддерживают, а сгенерированное сравнение учитываетEqualityContract, поэтому объекты разных типов иерархии не равны. - Наследование не единственный способ переиспользования. Композиция и интерфейсы дают меньше связности, наследование стоит брать под реальное отношение «является», а не ради экономии кода.
Где спотыкаются
Путают override и new и не могут предсказать вывод для Animal a = new Dog(). Второй частый прокол: вызов виртуального метода из конструктора базового класса, где переопределённая версия уже работает, а поля производного класса ещё не инициализированы.