Наследование, virtual, override, new
↑ 1.4 ООП в C♯ · ← Предыдущая · → Следующая
Зачем это на собесе
классический блок собеса —
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("Подпись врача");
}Вопросы с ответами
Чем override отличается от new?
override переопределяет виртуальный метод: вызывается версия реального типа объекта, даже через ссылку базового типа. new скрывает метод: какой вызвать, решается по типу переменной на этапе компиляции.
Что выведет Base b = new Derived(); b.Method(), если в Derived метод помечен new?
Версию из Base, потому что при сокрытии выбор идёт по типу переменной.
Чем абстрактный класс отличается от интерфейса?
Абстрактный класс может содержать состояние, конструкторы и реализацию, а наследовать можно только один класс. Интерфейс описывает контракт (с default-методами без состояния), и реализовать можно много интерфейсов.
Почему в C# методы по умолчанию не виртуальные?
Чтобы наследование было явным и осознанным (защита от хрупкого базового класса) и ради производительности: невиртуальный вызов дешевле и может быть заинлайнен.
Что такое паттерн Template Method?
Базовый класс задаёт скелет алгоритма в невиртуальном методе, а наследники переопределяют отдельные шаги (абстрактные или виртуальные методы).
Связанные темы
- Предыдущая: Модификаторы доступа, static, sealed, partial · Следующая: Интерфейсы и абстрактные классы, default interface methods
- Полиморфизм: Полиморфизм и инкапсуляция на практике
- Композиция вместо наследования: Композиция вместо наследования
- SOLID (LSP): [[be-8-1-1-solid-v-csharp-s-primerami|SOLID в C# с примерами]]
- Поведенческие паттерны: Поведенческие паттерны: Strategy, Chain of Responsibility, Command, Observer, Template Method, Mediator
- Прототипы в JS (сравнить): Прототипы и наследование