Наследование позволяет одному классу строиться на основе другого. Производный класс получает члены базового класса, добавляет свои и может заменить поведение, которое базовый класс разрешает заменить. Вместе с методами virtual оно даёт полиморфизм: код, написанный для базового класса, выполняет нужное поведение производного класса, не зная, какой именно производный класс ему достался.
Наследование класса
Укажите базовый класс после двоеточия. У производного класса есть всё, что есть у базового, плюс то, что он объявляет сам:
Вывод:
TX-19: 20 km, 2 fares
True
Что производный класс получает и чего не получает:
- Наследуется: поля, свойства, методы, события и вложенные типы. Физически всё это находится в объекте.
- Доступно: только то, что разрешает базовый класс: члены
public,protectedиinternal. ЧленprivateклассаVehicleсуществует внутри каждогоTaxi, но кодTaxiне может к нему обратиться по имени. Поэтому уKmестьprivate set:Taxiможет его читать, а меняет только черезDrive. - Не наследуется: конструкторы.
Taxiдолжен объявить свои и вызвать по цепочке один из конструкторовVehicleчерез: base(plate). Порядок их выполнения показан на странице о конструкторах.
Каждый класс в конечном счёте наследуется от object, поэтому у каждого объекта есть ToString(), Equals() и GetHashCode().
virtual и override
Базовый класс помечает метод virtual, чтобы сказать «производные классы могут предоставить свою версию». Производный класс заменяет его через override. Внутри переопределения base.Method() вызывает версию базового класса.
Вывод:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
Переменная цикла имеет тип Notification, и всё же каждый объект отображается по-своему. Это и есть полиморфизм: вызов n.Render(...) разрешается во время выполнения по фактическому типу объекта. Обратите также внимание, что base.Render в Email использует Channel, который сам виртуальный, поэтому базовый метод печатает email, а не generic. Виртуальный вызов внутри базового класса всё равно доходит до переопределения.
Свойства тоже могут быть виртуальными, как показывает Channel. Поля не могут.
Компилятор требует обоих ключевых слов. override на методе, который не является virtual, это ошибка CS0506 («cannot override inherited member ... because it is not marked virtual, abstract, or override»). Пропуск override, когда базовый метод виртуальный, это лишь предупреждение, но оно полностью меняет смысл, как показывает следующий раздел.
new или override: сокрытие вместо переопределения
Если производный класс объявляет метод с той же сигнатурой, что и базовый, не написав override, он скрывает базовый метод. Компилятор предупреждает (CS0114 для виртуального базового метода, CS0108 в остальных случаях) и предлагает ключевое слово new, которое убирает предупреждение, но сохраняет поведение сокрытия:
Вывод:
Sales report
Report
Draft report
b и c это объекты одного вида, но печатают они разные заголовки. С new выбранный метод зависит от типа переменной и определяется при компиляции. Код, который работает с отчётами как с Report (список, параметр метода, обратный вызов фреймворка), никогда не увидит DraftReport.Title. Почти никогда это не то, что нужно. Для полиморфизма используйте override; new существует в основном для случая, когда базовый класс, который вы не контролируете, добавляет член, имя которого совпадает с уже имеющимся у вас.
sealed
sealed на классе запрещает наследоваться от него:
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string запечатан, как и многие типы фреймворка. На переопределении sealed останавливает цепочку на этом уровне:
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
Проектирование класса для наследования требует работы: решить, что виртуальное, на что могут полагаться производные классы, в каком порядке всё происходит. Класс, который не проектировался так, безопаснее запечатать, причём запечатывание можно позже отменить, никого не сломав, а распечатывание нельзя вернуть назад, когда от вас уже наследуются. Вызовы членов запечатанных классов также могут быть немного быстрее, потому что среда выполнения знает, что переопределений нет.
Один базовый класс, много интерфейсов
У класса C# ровно один базовый класс. class Admin : User, Employee это ошибка CS1721 («cannot have multiple base classes»). Однако класс может реализовать любое число интерфейсов вместе с базовым классом:
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
Используйте базовый класс для «является разновидностью и разделяет реализацию», а интерфейсы для «умеет делать». Когда базовый класс нужен только для того, чтобы заставить производные классы заполнить некоторые методы, подходящий инструмент это абстрактный класс.
Приведение вверх и вниз по иерархии
Производный объект всегда можно использовать там, где ожидается его базовый тип. Такое восходящее приведение неявное и не может завершиться неудачей. В обратную сторону нисходящее приведение требует явного приведения и во время выполнения завершается ошибкой, если объект не того типа:
Вывод:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
Запись Dog d = pet; без приведения это ошибка компиляции (CS0266: «An explicit conversion exists (are you missing a cast?)»), потому что компилятор знает лишь, что pet это какое-то Animal. Предпочитайте is с переменной, когда объект может быть другого типа, а простое приведение используйте только тогда, когда что-либо другое было бы ошибкой. Частые нисходящие приведения это признак проблемы в проектировании: обычно это означает, что поведение должно находиться в виртуальном методе базового класса. Формы is полностью описаны на странице о сопоставлении с образцом.
Наследование и коллекции
Полиморфизм работает поэлементно, но обобщённые коллекции производного типа не являются коллекциями базового типа: List<Animal> animals = new List<Dog>(); не компилируется, потому что такой список тогда принял бы Cat. Представления только для чтения ковариантны, поэтому IEnumerable<Animal> animals = new List<Dog>(); допустимо. Почему так, объясняет страница об обобщениях.
Частые ошибки
- Забытый
override. Метод компилируется с предупреждением и молча скрывает вместо переопределения. Считайте CS0114 ошибкой. - Всё подряд
virtual. Каждый виртуальный член это обещание производным классам о том, когда он вызывается и на что может рассчитывать. Помечайте только те точки расширения, которые вы задумали. - Глубокие иерархии. Три-четыре уровня наследования затрудняют понимание того, какая версия метода выполняется. Композиция, когда класс хранит другой объект и вызывает его, часто проще нового уровня.
- Вызов виртуальных методов из конструктора. Переопределение выполняется до тела конструктора производного класса, поэтому любое поле, которое присваивает это тело, всё ещё содержит значение по умолчанию.
- Наследование только ради повторного использования кода. Если
Stackнаследуется отList, вызывающие смогут вызватьInsertв середину вашего стека. Вместо этого хранитеListв закрытом поле.
Часто задаваемые вопросы
Как работает наследование в C#?
Класс указывает один базовый класс после двоеточия: class Dog : Animal. Производный класс получает все члены базового (поля, свойства, методы, события), может добавить свои и может переопределить те, что базовый класс пометил virtual. Конструкторы не наследуются, а члены private, хотя и присутствуют в объекте, недоступны из производного класса.
Чем virtual отличается от override в C#?
virtual ставится на метод базового класса и говорит «производные классы могут его заменить». override ставится на метод производного класса и выполняет замену. Нужны оба: переопределение метода, который не является virtual, abstract или уже override, это ошибка CS0506. При вызове виртуального метода среда выполнения запускает версию для фактического типа объекта, а не типа переменной.
Чем new отличается от override в C#?
override заменяет базовый метод для всех вызывающих, даже для кода, который держит объект через переменную базового класса. new только скрывает его: код, который видит объект как производный тип, вызывает новый метод, а код, который видит его как базовый тип, по-прежнему вызывает базовый метод. Поэтому Base b = new Derived(); b.M(); выполняет Derived.M с override и Base.M с new.
Поддерживает ли C# множественное наследование?
Для классов нет: у класса ровно один базовый класс, и указание двух это ошибка CS1721. Класс может реализовать любое число интерфейсов, и именно так C# моделирует «этот тип умеет несколько вещей». Начиная с C# 8 интерфейсы могут также содержать реализации методов по умолчанию.
Что означает sealed в C#?
Класс sealed нельзя использовать как базовый; наследование от него это ошибка CS0509. Например, string запечатан. На методе sealed override запрещает классам ниже по иерархии переопределять его снова (CS0239). Запечатывать классы, которые не проектировались для наследования, разумно по умолчанию.
Как вызвать метод базового класса в C#?
Используйте base.MethodName(...) внутри производного класса, обычно внутри переопределения: public override string Describe() => base.Describe() + " with GPS";. Для конструкторов используйте : base(...) после списка параметров, чтобы выбрать, какой конструктор базового класса выполнится.