Наследование
Чему вы научитесь:
- объявлять производный класс и передавать аргументы конструктору базового через
base(…); - объяснять, почему вызов виртуального метода выбирает версию по настоящему типу объекта, и отличать переопределение
overrideот сокрытияnew; - описывать общий договор абстрактным классом или интерфейсом;
- проверять тип объекта через
isиas.

Представьте автопарк. В нём стоят легковые машины, грузовики и автобусы. Про любую машину парка можно сказать одно и то же: у неё есть название, и она умеет ехать. А у каждого вида есть и своё: легковая сигналит, грузовик возит груз, автобус — пассажиров.
При этом каждая легковая машина — транспорт, и каждый грузовик — тоже транспорт. Всё, что умеет транспорт вообще, умеет и легковая.
Как описать такой автопарк классами? Можно написать классы Car, Truck и Bus по отдельности. Тогда поле Name и метод Move придётся повторить в каждом из них, а ошибку в Move — исправлять три раза.
Эту задачу решает наследование: общее описывают один раз в одном классе, а другие классы получают его готовым и добавляют своё. Класс, от которого наследуют, называют базовым, а наследника — производным классом:
var car = new Car();
car.Name = "Lada";
car.Move(); // Lada moves
car.Honk(); // Lada: beep
class Vehicle
{
public string Name = "";
public void Move() => Console.WriteLine($"{Name} moves");
}
class Car : Vehicle
{
public void Honk() => Console.WriteLine($"{Name}: beep");
}Давайте разберём пример по строкам:
class Car : Vehicleобъявляет классCarпроизводным отVehicle. Двоеточие здесь читается как «является разновидностью»: легковая — разновидность транспорта.- В
Carне объявлены ни полеName, ни методMove, но у объектаnew Car()они есть: они пришли изVehicle. car.Move()вызывает метод базового класса и печатаетLada moves.car.Honk()вызывает собственный методCar. ВнутриHonkполеNameдоступно так же, как если бы его объявили в самомCar.
Что получает производный класс
Производный класс получает поля, свойства и методы базового. Закрытые члены private в объекте тоже есть, но код производного класса обратиться к ним не может.
Если базовый класс не указан, им становится object. Поэтому любой класс в C# — наследник object, даже если в его объявлении нет двоеточия.
Здесь аналогия с автопарком неточна. В жизни у машины бывает несколько «родителей»: она и транспорт, и чьё-то имущество, и товар в автосалоне. У класса в C# базовый класс только один, и код class Car : Vehicle, Machine не скомпилируется:
error CS1721: Class 'Car' cannot have multiple base classes: 'Vehicle' and 'Machine'Вопрос «Сколько базовых классов может быть у класса?» часто задают на собеседовании. Ответ для C# — один.
Сначала конструктор базового класса
Конструкторы не наследуются: у каждого класса свои. Когда создаётся объект производного класса, конструкторы выполняются по цепочке от базового к производному:
var bus = new Bus(); // Vehicle
// Bus
class Vehicle
{
public Vehicle() => Console.WriteLine("Vehicle");
}
class Bus : Vehicle
{
public Bus() => Console.WriteLine("Bus");
}Разберём, что происходит при new Bus():
- Вызывается конструктор
Bus. - Прежде чем выполнить его тело, C# вызывает конструктор базового класса
Vehicle, и тот печатаетVehicle. - Только потом выполняется тело конструктора
Bus: печатаетсяBus.
Поэтому, когда начинается тело производного конструктора, часть объекта от базового класса уже готова.
Вызов base(…)
У конструктора Vehicle из примера выше нет параметров, поэтому C# вызывает его сам. Если базовому конструктору нужны аргументы, конструктор производного класса передаёт их через : base(…) сразу после списка своих параметров:
var car = new Car("Lada");
car.Honk(); // Lada: beep
class Vehicle
{
protected string Name;
public Vehicle(string name) { Name = name; }
}
class Car : Vehicle
{
public Car(string name) : base(name) { }
public void Honk() => Console.WriteLine($"{Name}: beep");
}Давайте разберём по шагам:
new Car("Lada")вызывает конструкторCar, и в его параметрnameпопадает"Lada".: base(name)передаёт это значение конструкторуVehicle, и он выполняется первым: полеNameполучает"Lada".- Тело конструктора
Carпустое: всё нужное уже сделал базовый. car.Honk()печатаетLada: beep.
Конструкция : base(…) похожа на : this(…) из темы о классах. Только : this(…) вызывает другой конструктор того же класса, а : base(…) — конструктор базового.
Модификатор protected
Поле Name в этом примере объявлено как protected. Это модификатор доступа, который стоит между уже знакомыми public и private:
public— член виден всему коду;protected— член виден самому классу и его производным классам;private— член виден только самому классу.
Поэтому метод Honk в Car читает Name, а строка Console.WriteLine(car.Name); среди инструкций верхнего уровня не скомпилируется:
error CS0122: 'Vehicle.Name' is inaccessible due to its protection levelprotected подходит для того, что нужно наследникам, но не остальной программе.
Частая ошибка
В производном классе написали свой конструктор и не вызвали конструктор базового:
var car = new Car("Lada");
class Vehicle
{
public string Name;
public Vehicle(string name) { Name = name; }
}
class Car : Vehicle
{
public Car(string name) // ✗
{
Name = name;
}
}error CS7036: There is no argument given that corresponds to the required parameter 'name' of 'Vehicle.Vehicle(string)'Программа не компилируется. К конструктору без : base(…) C# сам добавляет вызов конструктора базового класса без аргументов. Но у Vehicle такого конструктора нет: ему нужен name. Та же ошибка появится, если у Car нет конструктора вовсе: конструктор по умолчанию тоже вызывает базовый без аргументов.
Исправление — public Car(string name) : base(name) { }: тогда конструктор Vehicle сам заполнит поле Name.
Главное
class Car : VehicleделаетCarпроизводным отVehicle: он получает члены базового класса и добавляет свои.- У класса в C# один базовый класс; если он не указан, это
object. - При создании объекта сначала выполняется конструктор базового класса, потом производного; аргументы базовому передают через
: base(…). protectedоткрывает член самому классу и его наследникам, но не остальному коду.