Наследование и ещё немного полиморфизма: 6‑я часть гайда по ООП
2026-02-21 16:46 Diff

#статьи

  • 6 дек 2019
  • 0

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

vlada_maestro / shutterstock

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Наследование в объектно-ориентированном программировании — это концепция, согласно которой одни классы, называемые родительскими, могут лежать в основе других — дочерних. При этом, дочерние классы перенимают свойства и поведение своего родителя.

Все живые существа наследуют черты своих родителей: цвет глаз или волос, форму лица, телосложение и т.д. При этом, какие-нибудь свойства, например, темперамент или физические качества, обязательно отличаются от таковых у родителей — иначе мы были бы копиями своих предков.

Другой, более технический пример — телефон и смартфон. Хотя у смартфона намного больше возможностей, чем у обыкновенной «звонилки», одну из них он точно унаследовал от телефона. И по 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, то при его объявлении после названия нужно поставить двоеточие и имя родительского класса:

Теперь объекты класса 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, который является родительским для всех остальных. У него есть четыре метода:

  • 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 (), при вызове которого у разных персонажей будут выводиться различные сообщения. Например, если атаковать будет маг, то мы должны увидеть сообщение, что он запустил огненный шар.

С помощью наследования можно создавать множество полезных классов с общим поведением и свойствами, при этом не дублируя код. Однако это ещё не всё, что можно использовать, — в следующей статье вы узнаете про интерфейсы и абстрактные классы.

Бесплатный курс по Python ➞
Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу