Наследование, virtual, override, new

↑ 1.4 ООП в C♯ · ← Предыдущая · → Следующая

Обязательно~7 мин чтенияУровень: junior

Зачем это на собесе

классический блок собеса — virtual/override против new, что выведет код с приведением к базовому типу, абстрактные классы. Плюс в проекте есть иерархии (базовые storage-сервисы, отчёты).

Объяснение

Наследование

Класс может наследовать один базовый класс (множественного наследования классов нет) и реализовать много интерфейсов.

public abstract class ReportBase
{
    protected ReportBase(string title) => Title = title;
    public string Title { get; }
 
    // Template Method: общий алгоритм, шаги переопределяются
    public byte[] Render(ReportData data)
    {
        Validate(data);
        var doc = BuildDocument(data);          // абстрактный шаг
        AddFooter(doc);                         // виртуальный шаг с реализацией по умолчанию
        return Export(doc);
    }
 
    protected abstract Document BuildDocument(ReportData data);   // обязаны реализовать
    protected virtual void AddFooter(Document doc) => doc.AddText($"Сформировано {DateTime.UtcNow:d}");
    protected virtual void Validate(ReportData data) => ArgumentNullException.ThrowIfNull(data);
    private byte[] Export(Document doc) => doc.ToPdf();
}
 
public sealed class DischargeSummaryReport() : ReportBase("Выписной эпикриз")
{
    protected override Document BuildDocument(ReportData data) => ...;
    protected override void AddFooter(Document doc)
    {
        base.AddFooter(doc);                                    // вызвать реализацию базового класса
        doc.AddText("Подпись врача: ________");
    }
}

virtual / override / new — главная ловушка

class Base    { public virtual string Who() => "Base";   public string Name() => "Base"; }
class Derived : Base
{
    public override string Who() => "Derived";           // переопределение (полиморфизм)
    public new string Name() => "Derived";               // сокрытие (НЕ полиморфизм)
}
 
Base b = new Derived();
b.Who();    // "Derived" — вызов по реальному типу объекта (виртуальная диспетчеризация)
b.Name();   // "Base"    — вызов по типу переменной (статическое связывание)
((Derived)b).Name();  // "Derived"
  • override — метод выбирается по типу объекта в рантайме (через таблицу виртуальных методов, vtable).
  • new — просто скрывает член базового класса. Выбор по типу переменной на этапе компиляции. Почти всегда признак проблемы в дизайне.
  • Без virtual в базе переопределить нельзя. Методы в C# по умолчанию невиртуальные (в отличие от Java).

abstract

  • Абстрактный класс нельзя инстанцировать.
  • Абстрактные члены не имеют реализации и обязаны быть переопределены в неабстрактном наследнике.
  • Может содержать состояние (поля), конструкторы, реализованные методы.

base

  • base.Method() — вызвать реализацию родителя.
  • : base(args) — вызвать конструктор родителя.

Upcasting и downcasting

ReportBase r = new DischargeSummaryReport();       // upcast — неявно и безопасно
if (r is DischargeSummaryReport ds) { ... }         // downcast — проверять через is

Нюансы и подводные камни

  • Хрупкий базовый класс: изменение базового класса ломает наследников неожиданным образом. Глубокие иерархии (больше 2–3 уровней) трудно поддерживать → композиция.
  • Нарушение LSP: наследник, который бросает NotSupportedException на методы базового, — признак неправильной иерархии.
  • new вместо override по ошибке (забыли virtual) — компилятор предупреждает CS0114. Не игнорируйте.
  • Вызов virtual в конструкторе базового класса — опасно (см. тему про конструкторы).
  • Для EF Core наследование сущностей маппится стратегиями TPH, TPT, TPC — у каждой свои компромиссы производительности.

Тестирование

[Fact]
public void Discharge_report_adds_signature_footer()
{
    var report = new DischargeSummaryReport();
    var pdf = report.Render(TestData.Discharge());
    PdfText(pdf).Should().Contain("Подпись врача");
}

Вопросы с ответами

Связанные темы