Наследование и ещё немного полиморфизма: 6‑я часть гайда по ООП
Вы всё время пользуетесь результатами наследования, даже если не знаете этого. Рассказываем, как меньше дублировать код и что общего у всех классов.


vlada_maestro / shutterstock
Оглавление:
- Как наследовать класс
- Добавление новых полей и методов
- Наследование конструкторов
- Переопределение методов
- Наследование от класса Object
- Особенности наследования
- Домашнее задание
- Заключение
Вот мы и подобрались к последнему столпу объектно-ориентированного программирования — наследованию. С его помощью можно создавать классы с общим функционалом, не копируя каждый раз одни и те же поля и методы.
Все статьи про ООП
Что такое наследование в ООП
Наследование в объектно-ориентированном программировании — это концепция, согласно которой одни классы, называемые родительскими, могут лежать в основе других — дочерних. При этом, дочерние классы перенимают свойства и поведение своего родителя.
Все живые существа наследуют черты своих родителей: цвет глаз или волос, форму лица, телосложение и т.д. При этом, какие-нибудь свойства, например, темперамент или физические качества, обязательно отличаются от таковых у родителей — иначе мы были бы копиями своих предков.
Другой, более технический пример — телефон и смартфон. Хотя у смартфона намного больше возможностей, чем у обыкновенной «звонилки», одну из них он точно унаследовал от телефона. И по Nokia 3310, и по IPhone 14, и по латвийскому VEF ТА-68 можно звонить другу и обсуждать новые эпизоды «Игры престолов».
Более ста лет с момента изобретения телефоны были проводными, а затем инженеры сделали их мобильными, то есть наделили новыми свойствами. Так на основе базового «Телефона» появился дочерний «Мобильный телефон». Потом кто-то засунул туда календарь, будильник, тетрис и интернет, или, как сказали бы программисты, добавил новых методов. Так появился класс «Смартфон», который лежит в основе большинства современных мобилок.
В программирование действуют по тому же принципу: мы создаем новые классы на основе родительских и наделяем их оригинальными свойствами и поведением.
Как наследовать класс
Для начала создадим класс, от которого будем наследовать. Обычно его называют базовым или родительским:
class Vehicle
{
public string name;
public int speed;
public int x;
public int y;
public void Move(Direction d)
{
switch(d)
{
case Direction.Forward:
y += speed;
break;
case Direction.Backward:
y -= speed;
break;
case Direction.Left:
x -= speed;
break;
case Direction.Right:
x += speed;
break;
}
}
}
Этот класс (Vehicle) представляет собой транспортное средство, но пока у него есть только слишком общие свойства (название, координаты и скорость) и поведение (перемещение). Нам может понадобиться реализовать класс, который тоже относится к транспортным средствам, но более конкретным. Например, это будет автомобиль (Car).
Если мы хотим, чтобы класс Car наследовал поля и методы класса Vehicle, то при его объявлении после названия нужно поставить двоеточие и имя родительского класса:
class Car : Vehicle
{
}
Теперь объекты класса Car обладают всеми полями и методами класса Vehicle:
Vehicle a = new Vehicle() //Создание экземпляра класса Vehicle
{
name = "Motorcycle",
speed = 2,
x = 5,
y = 10
};
Car b = new Car() //Создание экземпляра класса Car
{ //Используем те же поля
name = "Car",
speed = 3,
x = 15,
y = 40
};
//Используем одинаковые методы для обоих классов
a.Move(Direction.Forward);
b.Move(Direction.Left);
a = b; //Мы можем привести дочерний класс к типу родительского
//При этом стоит учитывать, что функционал и поля дочернего класса перестанут работать для этого объекта
Внимание! Наследовать можно только от одного класса.
Добавление новых полей и методов
Чтобы добавить в дочерний класс новое поле или метод, нужно просто объявить их:
class Car : Vehicle
{
public int horsePower = 1000;
public void Beep()
{
Console.WriteLine("Beep!");
}
}
Теперь объекты этого класса могут использовать как метод Move (), так и метод Beep (). То же самое касается и полей.
Наследование конструкторов
Допустим, у родительского класса есть конструктор, который принимает один аргумент:
public Vehicle(string name)
{
this.name = name;
}
Все дочерние классы должны вызывать его в своих конструкторах, передавая аргумент того же типа. Для этого используется ключевое слово base:
public Car(string name, int horsePower)
:base(name)
{
this.horsePower = horsePower;
}
В скобках после base указывается аргумент, который нужно передать в родительский класс. При этом повторно описывать логику присваивания name не нужно.
Если вы не хотите ничего вызывать, то просто создайте в наследуемом классе пустой конструктор.
Переопределение методов
Часто бывает нужно, чтобы какой-то метод в дочернем классе работал немного иначе, чем в родительском. Например, в методе Move () для класса Car можно прописать условие, которое будет проверять, не кончилось ли топливо. Точно так же может появиться необходимость переопределить свойство.
Методы и свойства, которые можно переопределить, называются виртуальными. В родительском классе для них указывается модификатор virtual:
public virtual void GetInfo()
{
Console.WriteLine($"Name: {name}\nSpeed: {speed}");
}
А в дочернем для переопределения используется модификатор override:
public override void GetInfo()
{
Console.WriteLine($"Name: {name}\nSpeed: {speed}\n Horse power: {horsePower}");
}
Таким образом можно определить разную логику для разных классов. Это тоже можно считать полиморфизмом.
Наследование от класса Object
Несмотря на то что наследовать можно только от одного класса, существует также и класс Object, который является родительским для всех остальных. У него есть четыре метода:
- Equals () — проверяет, равен ли текущий объект тому, что был передан в аргументе.
- ToString () — преобразует объект в строку.
- GetHashCode () — получает числовой хеш объекта. Этот метод редко используется, потому что может возвращать одинаковый хеш для разных объектов.
- GetType () — получает тип объекта.
Любой из них также может быть переопределён или перегружен. Например, метод Equals () можно использовать, чтобы он проверял, равны ли поля объектов:
public bool Equals(Car obj)
{
bool areEqual = false;
if(obj.name == this.name && obj.horsePower == this.horsePower)
{
areEqual = true;
}
return areEqual;
}
В данном случае это именно перегрузка, потому что ни один из вариантов метода Equals () не принимал объект класса Car. Отсюда следует, что переопределить можно только метод с такими же принимаемыми аргументами.
Особенности наследования
Есть несколько особенностей, которые нужно знать при работе с наследованием:
- Наследовать можно только от класса, уровень доступа которого выше дочернего или равен ему. То есть публичный класс не может наследоваться от приватного.
- Дочерний класс не может обращаться к приватным полям и методам родительского. Поэтому нужно либо определять логику приватных компонентов в базовом классе, либо создавать публичные свойства и методы, которые будут своего рода посредниками.
- У дочернего класса может быть только один родительский, но у родительского может быть несколько дочерних.
- Нельзя наследовать от класса с модификатором static.
- Можно наследовать от класса, который наследует от другого класса. Но с этим лучше не злоупотреблять, потому что можно быстро запутаться в их взаимосвязях.
Чтобы лучше это усвоить, стоит попробовать поработать с каждой особенностью на практике и немного поэкспериментировать.
Домашнее задание
Создайте несколько классов персонажей: например, воин, лучник и маг.
Каждый из них должен быть родительским для нескольких других классов допустим, воин будет базовым классом для рыцаря и берсеркера.
У всех персонажей должен быть метод Attack (), при вызове которого у разных персонажей будут выводиться различные сообщения. Например, если атаковать будет маг, то мы должны увидеть сообщение, что он запустил огненный шар.
Заключение
С помощью наследования можно создавать множество полезных классов с общим поведением и свойствами, при этом не дублируя код. Однако это ещё не всё, что можно использовать, — в следующей статье вы узнаете про интерфейсы и абстрактные классы.
Больше интересного про код в нашем телеграм-канале. Подписывайтесь!