Как реализовано наследование классов в .NET?

Ответ

Наследование в .NET реализуется на уровне CLR и поддерживает одиночное наследование реализации (один базовый класс) и множественное наследование интерфейсов.

Базовый синтаксис:

public class Vehicle // Базовый класс
{
    public string Manufacturer { get; set; }
    public virtual void StartEngine() => Console.WriteLine("Vehicle engine started.");
}

public class Car : Vehicle // Производный класс
{
    public int DoorCount { get; set; }
    public override void StartEngine() => Console.WriteLine("Car engine roars!");
}

Ключевые аспекты реализации в CLR:

  1. Единый корень: Все классы неявно наследуются от System.Object. Это обеспечивает наличие общих методов (ToString(), GetHashCode(), Equals()).
  2. Таблица виртуальных методов (VMT): Каждый тип имеет таблицу, содержащую адреса виртуальных методов. При переопределении (override) в производном классе соответствующая запись в его VMT заменяется на адрес новой реализации. Это обеспечивает полиморфное поведение.
  3. Вызов конструкторов: Конструкторы не наследуются. При создании объекта производного класса цепочка конструкторов вызывается в порядке от базового к производному.
public class BaseClass
{
    public BaseClass() { Console.WriteLine("Base constructor"); }
}

public class DerivedClass : BaseClass
{
    // Компилятор неявно вставляет вызов base()
    public DerivedClass() { Console.WriteLine("Derived constructor"); }
}
// Вывод: Base constructor -> Derived constructor
  1. Модификаторы:
    • sealed — запрещает наследование от класса или дальнейшее переопределение метода.
    • abstract — класс нельзя инстанцировать, он предназначен только для наследования.
    • virtual/override — для полиморфного поведения.
    • new — для сокрытия (hiding) члена базового класса (создаёт новый, независимый член, не участвующий в полиморфизме).

Наследование vs. Композиция: В практике .NET-разработки часто предпочтение отдаётся композиции (включению объектов других классов) над наследованием для повышения гибкости и уменьшения связности кода, следуя принципу "предпочитайте композицию наследованию".

Ответ 18+ 🔞

Да ты посмотри, какая красота! Наследование в .NET — это как взять тачку отца, но накрутить на неё нитро и спойлер. Всё родное остаётся, но ты уже едешь быстрее и с другим звуком.

Смотри, как это выглядит в коде, тут всё просто, как два пальца:

public class Vehicle // Это батя. Его тачка.
{
    public string Manufacturer { get; set; }
    public virtual void StartEngine() => Console.WriteLine("Двигатель завёлся. Трррр.");
}

public class Car : Vehicle // А это я. Унаследовал тачку.
{
    public int DoorCount { get; set; }
    public override void StartEngine() => Console.WriteLine("ВРУМ-ВРУМ! Опа, поехали!"); // Переделал под себя.
}

А теперь, блядь, самое интересное — как эта магия под капотом работает. CLR тут не хухры-мухры, всё по уму сделано.

  1. Все мы дети System.Object. Это как все мы в итоге вышли из обезьяны. Каждый твой класс, даже если ты этого не видишь, уже имеет методы вроде ToString() или GetHashCode(). Это база, хуле.

  2. Таблица виртуальных методов (VMT) — сердце полиморфизма. Представь себе телефонную книжку для методов. У базового класса своя книжка: "StartEngine — звони сюда". Когда ты делаешь override, производный класс берёт эту книжку и поверх старой записи пишет: "StartEngine — не, братан, теперь звони сюда, по новому адресу". Поэтому когда вызываешь метод у объекта через ссылку на базовый класс, CLR лезет в книжку реального типа объекта и находит правильный номер. Красота!

  3. Конструкторы — цепочка вызовов. Конструкторы, сука, не наследуются. Это как нельзя унаследовать право первой ночи, извини. При создании объекта всё идёт по цепочке снизу вверх:

    public class BaseClass
    {
        public BaseClass() { Console.WriteLine("Конструктор бати."); }
    }
    
    public class DerivedClass : BaseClass
    {
        // Компилятор сам, тихонечко, добавит вызов base()
        public DerivedClass() { Console.WriteLine("Конструктор сына."); }
    }
    // Вывод: Конструктор бати. -> Конструктор сына.

    Сначала батя всё подготовит, потом уже ты своё накручиваешь. Иначе можно ебнуться.

  4. Модификаторы — наши регуляторы. Тут целый арсенал:

    • sealed — поставил на класс, и всё, приехали. От него больше никто родиться не может. Как стерилизация. Для метода — дальше переопределять нельзя.
    • abstract — класс-призрак. Создать его экземпляр нельзя, только наследоваться. Он как идея, чертёж.
    • virtual/override — классика для полиморфизма. Сделал метод виртуальным — дал детям шанс его переписать.
    • new — вот это, блядь, опасная штука. Сокрытие. Ты не переопределяешь метод родителя, ты создаёшь рядом совершенно новый, одноимённый метод. И если будешь вызывать его через ссылку на базовый класс — работать будет старый метод бати. Частая причина, блядь, "ну почему не вызывается?!" на code review. Используй с умом, а лучше — с перепугу.

И главное, запомни, как Отче наш: в нормальной жизни композицию часто предпочитают наследованию. Наследование — это жёсткая сцепка, "is-a" (автомобиль является транспортным средством). А композиция — "has-a" (автомобиль имеет двигатель). Гибче, проще менять, меньше проблем. Не лезь с наследованием, где можно просто взять и включить один объект в другой. Это, блядь, как брать в долг у друга, а не у банка с его суровыми процентами-обязательствами.