Перейти к содержимому
Теория · Наследование и интерфейсы · урок 3 из 10

virtual и override

Вернёмся в автопарк. У диспетчера одно правило для всего транспорта: «По команде назови себя». Базовая версия правила отвечает «транспорт». Но каждый вид транспорта может переписать его под себя: легковая ответит «легковая», грузовик — «грузовик».

Диспетчер не разглядывает, кто перед ним. Он даёт одну команду всем, а каждый выполняет свою версию правила.

В C# роль диспетчера играет код, который работает с переменной базового типа. В переменную типа Vehicle можно положить объект Car: любая легковая машина — транспорт. Какую версию метода выполнит вызов через такую переменную?

C#
Vehicle v = new Car();
Console.WriteLine(v.Who()); // car

class Vehicle
{
    public virtual string Who() => "vehicle";
}

class Car : Vehicle
{
    public override string Who() => "car";
}

Давайте разберём по строкам:

  1. Vehicle v = new Car(); — у переменной тип Vehicle, а у объекта, к которому она ведёт, тип Car.
  2. virtual в Vehicle разрешает производным классам переписать метод Who.
  3. override в Car переписывает его.
  4. v.Who() печатает car: вызов выполнил версию объекта, хотя у переменной тип Vehicle.

Тип переменной и тип объекта

В вызове v.Who() участвуют два типа, и каждый решает своё:

  • тип переменной, Vehicle, решает, какие методы можно вызвать: компилятор проверяет, что Who есть в Vehicle;
  • тип объекта, Car, решает, какая версия выполнится: это выясняется при запуске, когда объект уже создан.

Метод с virtual называют виртуальным, а его новую версию с override в производном классе — переопределением. Способность одного и того же вызова выполнять разный код для объектов разных типов называется полиморфизмом. Об этом часто спрашивают на собеседовании: «Что такое полиморфизм и как он устроен в C#?»

В виджете одна и та же переменная v типа Vehicle ведёт по очереди к трём объектам. Надпись → Car { } читается так же, как в теме о классах: v ведёт к объекту Car, а пустые скобки значат, что полей у него нет. Нажимайте «Следующий шаг» и смотрите на верхнюю рамку: в её заголовке написано, чья версия Who выполняется.

ВживуюВызов идёт за объектомшаг 1 из 9 · рамок: 1

Переменная v типа Vehicle ведёт к объекту Vehicle.

Заголовок верхней рамки показывает, чья версия Who выполняется

Тип переменной за всю программу не изменился, а выполнялись разные версии Who. Если производный класс, как Bus, не переопределил виртуальный метод, для его объекта выполняется версия, унаследованная от базового класса.

Здесь аналогия с диспетчером неточна. В автопарке любой вид транспорта может переписать любое правило, а в C# переопределить можно только виртуальный метод — помеченный словом virtual в базовом классе.

Вызов базовой версии: base

Переопределение не обязано выбрасывать базовую версию. Внутри метода с override её можно вызвать через base.:

C#
Vehicle t = new Taxi();
Console.WriteLine(t.Who()); // taxi, a car

class Taxi : Car
{
    public override string Who() => "taxi, a " + base.Who();
}

Это продолжение первой программы урока: инструкции добавьте после её инструкций, а класс Taxi — в конец файла. Разберём, что происходит:

  1. Taxi наследует от Car, а Car — от Vehicle. Метод с override сам остаётся виртуальным, поэтому Taxi может переопределить версию Car.
  2. t.Who() выполняет версию объекта, то есть Taxi.Who.
  3. У Taxi базовый класс — Car, и base указывает на него. Поэтому base.Who() выполняет версию Car и возвращает "car".
  4. Taxi.Who добавляет к ней своё начало, и печатается taxi, a car.

Переопределение ToString

Любой класс — наследник object, а в object есть виртуальный метод ToString. Его вызывает Console.WriteLine, когда печатает объект. Версия из object возвращает имя типа объекта. Переопределите ToString, и Console.WriteLine будет печатать то, что вы задали:

C#
Console.WriteLine(v);    // Car
var bike = new Bike("BMX");
Console.WriteLine(bike); // Bike BMX

class Bike : Vehicle
{
    public string Model;
    public Bike(string model) { Model = model; }
    public override string ToString() => $"Bike {Model}";
}

Этот фрагмент тоже продолжает первую программу:

  1. Console.WriteLine(v) печатает Car. В Car нет своего ToString, и работает версия из object. Обратите внимание: напечатано имя типа объекта, а не переменной.
  2. В Bike метод ToString переопределён, поэтому Console.WriteLine(bike) печатает Bike BMX.

Частая ошибка

Переопределили метод, который в базовом классе не помечен virtual:

C#
Vehicle v = new Car();
Console.WriteLine(v.Who());

class Vehicle
{
    public string Who() => "vehicle";
}

class Car : Vehicle
{
    public override string Who() => "car"; // ✗
}
TEXT
error CS0506: 'Car.Who()': cannot override inherited member 'Vehicle.Who()' because it is not marked virtual, abstract, or override

Программа не компилируется. В C# метод по умолчанию не виртуальный, а Vehicle не пометил Who словом virtual и поэтому не разрешил его переписывать. Добавьте virtual в объявление Who в классе Vehicle, как в первом примере.

Главное

  • virtual разрешает переписать метод в производном классе, а override переписывает его.
  • Тип переменной решает, какие методы можно вызвать, а тип объекта — какая версия виртуального метода выполнится.
  • base.Who() внутри переопределения вызывает версию базового класса.
  • Console.WriteLine(obj) вызывает ToString; без переопределения тот возвращает имя типа объекта.