virtual и override
Вернёмся в автопарк. У диспетчера одно правило для всего транспорта: «По команде назови себя». Базовая версия правила отвечает «транспорт». Но каждый вид транспорта может переписать его под себя: легковая ответит «легковая», грузовик — «грузовик».
Диспетчер не разглядывает, кто перед ним. Он даёт одну команду всем, а каждый выполняет свою версию правила.
В C# роль диспетчера играет код, который работает с переменной базового типа. В переменную типа Vehicle можно положить объект Car: любая легковая машина — транспорт. Какую версию метода выполнит вызов через такую переменную?
Vehicle v = new Car();
Console.WriteLine(v.Who()); // car
class Vehicle
{
public virtual string Who() => "vehicle";
}
class Car : Vehicle
{
public override string Who() => "car";
}Давайте разберём по строкам:
Vehicle v = new Car();— у переменной типVehicle, а у объекта, к которому она ведёт, типCar.virtualвVehicleразрешает производным классам переписать методWho.overrideвCarпереписывает его.v.Who()печатаетcar: вызов выполнил версию объекта, хотя у переменной типVehicle.
Тип переменной и тип объекта
В вызове v.Who() участвуют два типа, и каждый решает своё:
- тип переменной,
Vehicle, решает, какие методы можно вызвать: компилятор проверяет, чтоWhoесть вVehicle; - тип объекта,
Car, решает, какая версия выполнится: это выясняется при запуске, когда объект уже создан.
Метод с virtual называют виртуальным, а его новую версию с override в производном классе — переопределением. Способность одного и того же вызова выполнять разный код для объектов разных типов называется полиморфизмом. Об этом часто спрашивают на собеседовании: «Что такое полиморфизм и как он устроен в C#?»
В виджете одна и та же переменная v типа Vehicle ведёт по очереди к трём объектам. Надпись → Car { } читается так же, как в теме о классах: v ведёт к объекту Car, а пустые скобки значат, что полей у него нет. Нажимайте «Следующий шаг» и смотрите на верхнюю рамку: в её заголовке написано, чья версия Who выполняется.
1Vehicle v = new Vehicle();2Console.WriteLine(v.Who());3v = new Car();4Console.WriteLine(v.Who());5v = new Bus();6Console.WriteLine(v.Who());7 8class Vehicle9{10 public virtual string Who()11 {12 return "vehicle";13 }14}15 16class Car : Vehicle17{18 public override string Who()19 {20 return "car";21 }22}23 24class Bus : Vehicle { }
Переменная v типа Vehicle ведёт к объекту Vehicle.
Тип переменной за всю программу не изменился, а выполнялись разные версии Who. Если производный класс, как Bus, не переопределил виртуальный метод, для его объекта выполняется версия, унаследованная от базового класса.
Здесь аналогия с диспетчером неточна. В автопарке любой вид транспорта может переписать любое правило, а в C# переопределить можно только виртуальный метод — помеченный словом virtual в базовом классе.
Вызов базовой версии: base
Переопределение не обязано выбрасывать базовую версию. Внутри метода с override её можно вызвать через base.:
Vehicle t = new Taxi();
Console.WriteLine(t.Who()); // taxi, a car
class Taxi : Car
{
public override string Who() => "taxi, a " + base.Who();
}Это продолжение первой программы урока: инструкции добавьте после её инструкций, а класс Taxi — в конец файла. Разберём, что происходит:
Taxiнаследует отCar, аCar— отVehicle. Метод сoverrideсам остаётся виртуальным, поэтомуTaxiможет переопределить версиюCar.t.Who()выполняет версию объекта, то естьTaxi.Who.- У
Taxiбазовый класс —Car, иbaseуказывает на него. Поэтомуbase.Who()выполняет версиюCarи возвращает"car". Taxi.Whoдобавляет к ней своё начало, и печатаетсяtaxi, a car.
Переопределение ToString
Любой класс — наследник object, а в object есть виртуальный метод ToString. Его вызывает Console.WriteLine, когда печатает объект. Версия из object возвращает имя типа объекта. Переопределите ToString, и Console.WriteLine будет печатать то, что вы задали:
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}";
}Этот фрагмент тоже продолжает первую программу:
Console.WriteLine(v)печатаетCar. ВCarнет своегоToString, и работает версия изobject. Обратите внимание: напечатано имя типа объекта, а не переменной.- В
BikeметодToStringпереопределён, поэтомуConsole.WriteLine(bike)печатаетBike BMX.
Частая ошибка
Переопределили метод, который в базовом классе не помечен virtual:
Vehicle v = new Car();
Console.WriteLine(v.Who());
class Vehicle
{
public string Who() => "vehicle";
}
class Car : Vehicle
{
public override string Who() => "car"; // ✗
}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; без переопределения тот возвращает имя типа объекта.