Как реализовано переопределение методов в C#?

Ответ

Переопределение (override) в C# — это механизм, позволяющий производному классу предоставить свою собственную реализацию виртуального или абстрактного метода, объявленного в базовом классе. Реализация основана на таблице виртуальных методов (Virtual Method Table, VMT).

Синтаксис и правила:

public class LoggerBase
{
    // 1. Метод должен быть помечен как virtual, abstract или уже override
    public virtual void Log(string message)
    {
        Console.WriteLine($"[BASE] {DateTime.Now}: {message}");
    }
}

public class FileLogger : LoggerBase
{
    // 2. Сигнатура метода (имя, тип возвращаемого значения, параметры) должна полностью совпадать
    // 3. Модификатор доступа должен быть таким же или менее строгим
    public override void Log(string message)
    {
        // 4. Можно обратиться к реализации базового класса через base
        base.Log(message); // Вызовет LoggerBase.Log
        // Новая логика для записи в файл
        File.AppendAllText("log.txt", message + "n");
    }
}

Что происходит под капотом?

  1. Класс LoggerBase имеет в своей VMT запись, указывающую на реализацию LoggerBase.Log.
  2. Класс FileLogger создаёт свою VMT. Для метода Log запись указывает на новую реализацию (FileLogger.Log).
  3. При вызове logger.Log(...) где logger имеет тип LoggerBase, но ссылается на объект FileLogger, CLR смотрит в VMT реального типа объекта (FileLogger) и вызывает соответствующую реализацию. Это и есть полиморфизм.

Отличия от сокрытия (new):

public class A { public void Method() => Console.WriteLine("A"); }
public class B : A { public new void Method() => Console.WriteLine("B"); } // Сокрытие

A obj = new B();
obj.Method(); // Выведет "A"! Выбор метода определяется типом переменной (A), а не объекта.
((B)obj).Method(); // Выведет "B"

Ключевые моменты:

  • sealed override — запрещает дальнейшее переопределение метода в цепочке наследования.
  • Переопределять можно только виртуальные члены. Попытка переопределить невиртуальный метод приведёт к ошибке компиляции.
  • Свойства и индексаторы также могут быть виртуальными и переопределяемыми.

Ответ 18+ 🔞

А, переопределение методов! Ну это ж классика, как водка под селёдку — без этого нихуя не работает нормально. Смотри, объясню на пальцах, а то в учебниках такое заумно пишут, что мозг вытекает.

Представь, есть у тебя базовый класс, типа общего предка. Ну, как твой дед, который всем рассказывает, как надо правильно картошку сажать. У него метод Log есть, виртуальный:

public class LoggerBase
{
    public virtual void Log(string message)
    {
        Console.WriteLine($"[BASE] {DateTime.Now}: {message}");
    }
}

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

И тут появляется его продвинутый внук-айтишник, FileLogger:

public class FileLogger : LoggerBase
{
    public override void Log(string message)
    {
        base.Log(message); // Это типа «деда, ты тоже скажи своё слово»
        File.AppendAllText("log.txt", message + "n"); // А это уже своя, новая фишка
    }
}

Ключевое слово override — это как внук говорит: «Дед, я твой метод уважаю, но ща сделаю круче, с записью в файл». И система это понимает и разрешает.

А теперь самое важное, где все путаются — что под капотом-то происходит?

Есть эта самая таблица виртуальных методов (VMT). Это как общая шпаргалка для всех объектов класса. Когда ты создаёшь FileLogger, система смотрит: «Так, у этого парня есть переопределённый метод Log — окей, в таблицу записываем адрес его новой реализации, а не дедовой».

И когда ты вызываешь метод через ссылку на базовый класс:

LoggerBase logger = new FileLogger();
logger.Log("Тестовое сообщение"); // Вызовется FileLogger.Log!

Система не смотрит на тип переменной (LoggerBase). Она лезет в таблицу реального объекта (который FileLogger), находит там свежую, актуальную реализацию и запускает её. Это и есть полиморфизм, ёпта! Всё работает так, как и должно — умно и правильно.

А теперь смотри, чем это НЕ является:

Есть ещё такая пакость — сокрытие методов через new. Вот это реально подстава.

public class A { public void Method() => Console.WriteLine("A"); }
public class B : A { public new void Method() => Console.WriteLine("B"); }

A obj = new B();
obj.Method(); // Выведет "A"! Охуеть, да?
((B)obj).Method(); // А вот тут уже "B"

Видишь? Слово new — это как внук взял и сделал метод с таким же названием, но деду не сказал. И когда ты обращаешься к объекту как к «деду» (тип A), то вызывается старый метод. А чтобы вызвать новый, надо прямо указать, что ты работаешь с «внуком» (тип B). Полная хуйня, если хотел полиморфизма. Это не переопределение, а просто два разных метода, которые случайно называются одинаково. Путаница и пиздец.

Итоговые правила, чтобы не облажаться:

  1. Переопределять (override) можно только то, что помечено как virtual, abstract или уже является override.
  2. Сигнатура (имя, параметры, возвращаемый тип) должна совпадать точь-в-точь.
  3. Можно позвать реализацию предка через base.Метод(), если надо.
  4. Если переопределил и хочешь, чтобы дальше по цепочке наследования этот метод уже НЕ трогали — пометь его sealed override. Поставил точку, тема закрыта.
  5. Переопределять можно не только методы, но и свойства, и индексаторы. Главное, чтобы они тоже были виртуальными.

Вот и вся магия. Если короче: virtual/override — для честного, умного полиморфизма. new — для сокрытия, когда надо создать отдельный, независимый метод, но обычно это признак кривого дизайна. Выбирай первый вариант, и будет тебе счастье.