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

Наследование

Чему вы научитесь

Чему вы научитесь:

  • объявлять производный класс и передавать аргументы конструктору базового через base(…);
  • объяснять, почему вызов виртуального метода выбирает версию по настоящему типу объекта, и отличать переопределение override от сокрытия new;
  • описывать общий договор абстрактным классом или интерфейсом;
  • проверять тип объекта через is и as.
Шасси Vehicle сверху и три машины на таком же шасси: легковая Car с клаксоном Honk(), грузовик и автобус

Представьте автопарк. В нём стоят легковые машины, грузовики и автобусы. Про любую машину парка можно сказать одно и то же: у неё есть название, и она умеет ехать. А у каждого вида есть и своё: легковая сигналит, грузовик возит груз, автобус — пассажиров.

При этом каждая легковая машина — транспорт, и каждый грузовик — тоже транспорт. Всё, что умеет транспорт вообще, умеет и легковая.

Как описать такой автопарк классами? Можно написать классы Car, Truck и Bus по отдельности. Тогда поле Name и метод Move придётся повторить в каждом из них, а ошибку в Move — исправлять три раза.

Эту задачу решает наследование: общее описывают один раз в одном классе, а другие классы получают его готовым и добавляют своё. Класс, от которого наследуют, называют базовым, а наследника — производным классом:

C#
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");
}

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

  1. class Car : Vehicle объявляет класс Car производным от Vehicle. Двоеточие здесь читается как «является разновидностью»: легковая — разновидность транспорта.
  2. В Car не объявлены ни поле Name, ни метод Move, но у объекта new Car() они есть: они пришли из Vehicle.
  3. car.Move() вызывает метод базового класса и печатает Lada moves.
  4. car.Honk() вызывает собственный метод Car. Внутри Honk поле Name доступно так же, как если бы его объявили в самом Car.

Что получает производный класс

Производный класс получает поля, свойства и методы базового. Закрытые члены private в объекте тоже есть, но код производного класса обратиться к ним не может.

Если базовый класс не указан, им становится object. Поэтому любой класс в C# — наследник object, даже если в его объявлении нет двоеточия.

Здесь аналогия с автопарком неточна. В жизни у машины бывает несколько «родителей»: она и транспорт, и чьё-то имущество, и товар в автосалоне. У класса в C# базовый класс только один, и код class Car : Vehicle, Machine не скомпилируется:

TEXT
error CS1721: Class 'Car' cannot have multiple base classes: 'Vehicle' and 'Machine'

Вопрос «Сколько базовых классов может быть у класса?» часто задают на собеседовании. Ответ для C# — один.

Сначала конструктор базового класса

Конструкторы не наследуются: у каждого класса свои. Когда создаётся объект производного класса, конструкторы выполняются по цепочке от базового к производному:

C#
var bus = new Bus(); // Vehicle
                     // Bus

class Vehicle
{
    public Vehicle() => Console.WriteLine("Vehicle");
}

class Bus : Vehicle
{
    public Bus() => Console.WriteLine("Bus");
}

Разберём, что происходит при new Bus():

  1. Вызывается конструктор Bus.
  2. Прежде чем выполнить его тело, C# вызывает конструктор базового класса Vehicle, и тот печатает Vehicle.
  3. Только потом выполняется тело конструктора Bus: печатается Bus.

Поэтому, когда начинается тело производного конструктора, часть объекта от базового класса уже готова.

Вызов base(…)

У конструктора Vehicle из примера выше нет параметров, поэтому C# вызывает его сам. Если базовому конструктору нужны аргументы, конструктор производного класса передаёт их через : base(…) сразу после списка своих параметров:

C#
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");
}

Давайте разберём по шагам:

  1. new Car("Lada") вызывает конструктор Car, и в его параметр name попадает "Lada".
  2. : base(name) передаёт это значение конструктору Vehicle, и он выполняется первым: поле Name получает "Lada".
  3. Тело конструктора Car пустое: всё нужное уже сделал базовый.
  4. car.Honk() печатает Lada: beep.

Конструкция : base(…) похожа на : this(…) из темы о классах. Только : this(…) вызывает другой конструктор того же класса, а : base(…) — конструктор базового.

Модификатор protected

Поле Name в этом примере объявлено как protected. Это модификатор доступа, который стоит между уже знакомыми public и private:

  • public — член виден всему коду;
  • protected — член виден самому классу и его производным классам;
  • private — член виден только самому классу.

Поэтому метод Honk в Car читает Name, а строка Console.WriteLine(car.Name); среди инструкций верхнего уровня не скомпилируется:

TEXT
error CS0122: 'Vehicle.Name' is inaccessible due to its protection level

protected подходит для того, что нужно наследникам, но не остальной программе.

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

В производном классе написали свой конструктор и не вызвали конструктор базового:

C#
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;
    }
}
TEXT
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 открывает член самому классу и его наследникам, но не остальному коду.