Наследование и ещё немного полиморфизма: 6‑я часть гайда по ООП
Вы всё время пользуетесь результатами наследования, даже если не знаете этого. Рассказываем, как меньше дублировать код и что общего у всех классов.
Евгений Кучерявый
Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Оглавление:
- Как наследовать класс
- Добавление новых полей и методов
- Наследование конструкторов
- Переопределение методов
- Наследование от класса Object
- Особенности наследования
- Домашнее задание
- Заключение
Вот мы и подобрались к последнему столпу объектно-ориентированного программирования — наследованию. С его помощью можно создавать классы с общим функционалом, не копируя каждый раз одни и те же поля и методы.
Все статьи про ООП
- Что такое классы и объекты.
- Особенности работы с объектами.
- Модификаторы доступа, инкапсуляция.
- Полиморфизм и перегрузка методов.
- Полиморфизм.
- Наследование и ещё немного полиморфизма.
- Абстрактные классы и интерфейсы.
- Практикум.
Что такое наследование в ООП
Наследование в объектно-ориентированном программировании — это концепция, согласно которой одни классы, называемые родительскими, могут лежать в основе других — дочерних. При этом, дочерние классы перенимают свойства и поведение своего родителя.
Все живые существа наследуют черты своих родителей: цвет глаз или волос, форму лица, телосложение и т.д. При этом, какие-нибудь свойства, например, темперамент или физические качества, обязательно отличаются от таковых у родителей — иначе мы были бы копиями своих предков.
Другой, более технический пример — телефон и смартфон. Хотя у смартфона намного больше возможностей, чем у обыкновенной «звонилки», одну из них он точно унаследовал от телефона. И по Nokia 3310, и по IPhone 14, и по латвийскому VEF ТА-68 можно звонить другу и обсуждать новые эпизоды «Игры престолов».
Более ста лет с момента изобретения телефоны были проводными, а затем инженеры сделали их мобильными, то есть наделили новыми свойствами. Так на основе базового «Телефона» появился дочерний «Мобильный телефон». Потом кто-то засунул туда календарь, будильник, тетрис и интернет, или, как сказали бы программисты, добавил новых методов. Так появился класс «Смартфон», который лежит в основе большинства современных мобилок.
В программирование действуют по тому же принципу: мы создаем новые классы на основе родительских и наделяем их оригинальными свойствами и поведением.
Как наследовать класс
Для начала создадим класс, от которого будем наследовать. Обычно его называют базовым или родительским:
Теперь объекты этого класса могут использовать как метод 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: \nSpeed: "); >
А в дочернем для переопределения используется модификатор override:
public override void GetInfo( ) < Console.WriteLine($"Name: \nSpeed: \n Horse power: "); >
Таким образом можно определить разную логику для разных классов. Это тоже можно считать полиморфизмом.
Наследование от класса 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 (), при вызове которого у разных персонажей будут выводиться различные сообщения. Например, если атаковать будет маг, то мы должны увидеть сообщение, что он запустил огненный шар.
Заключение
С помощью наследования можно создавать множество полезных классов с общим поведением и свойствами, при этом не дублируя код. Однако это ещё не всё, что можно использовать, — в следующей статье вы узнаете про интерфейсы и абстрактные классы.
Больше интересного про код в нашем телеграм-канале. Подписывайтесь!
Читайте также:
- Не Windows единой: как писать кроссплатформенные приложения с GUI на C#
- Улучшаем CV: инструкция от опытного разработчика
- Аргументы запуска в C#: что это и как их применять
Хеш — результат преобразования данных, который используется в криптографии.
Наследование в объектно-ориентированном программировании
Допустим, в программе должны быть объекты, поле number которых можно только увеличивать и уменьшать на величину шага. Также в программе нужны объекты, у которых number может изменяться не только добавлением/вычитанием шага, но также умножением на шаг. Конечно, мы можем написать еще один класс:
class NumMult(n: Int, gap: Int) { var number = n var step = gap fun inc() {number += step} fun dec() {number -= step} fun mult() {number *= step} }
Однако он во многом повторяет предыдущий класс, поэтому имеет смысл сделать его дочерним по отношению к NumInt , который выступит в роли родительского. В ООП дочерний класс наследует свойства и метода родительского. Таким образом, в NumMult нам придется описывать только дополнительную функциональность. Другими словами, дочерний наследует особенности родительского, а также расширяет, дополняет их.
Чтобы класс мог быть родительским перед его объявлением должно стоять ключевое слово open .
open class NumInc(n: Int, gap: Int) { .
В свою очередь класс-наследник должен в своем заголовке иметь запись о родительском классе. В нашем случае определение класса NumMult будет выглядеть так:
class NumMult(num: Int, coef: Int): NumInc(num, coef) { fun mult() {number *= step} }
В заголовке после параметров первичного конструктора (если он есть) ставится двоеточие, после которого идет имя родительского класса. Поскольку конструктор родительского класса предусматривает два параметра, мы должны их туда передать.
После этого объекты NumMult будут обладать теми же свойствами и методами, что и объекты NumInc . У них тоже появятся свойства number и step , методы inc() и dec() . Однако помимо этого у них есть метод mult() , которого нет у объектов родительского класса.
Наследование может быть сложнее. Дочерний класс может стать родительским по отношению к другому дочернему. Для этого перед его объявлением также должно стоять слово open .
В ряде языков программирования дочерний класс может наследовать от нескольких родительских. В Kotlin так делать нельзя, у подкласса всегда один надкласс. Проблема же множественного наследования решается через интерфейсы, которые будут рассмотрены позже.
Давайте усложним наш пример, введя в дочерний класс третье свойство.
class NumMult(num: Int, gap: Int, coef: Int): NumInc(num, gap) { var coefficient = coef fun mult() {number *= coefficient} }
Теперь дочерний класс обладает не только дополнительным методом, но и дополнительным полем. Конструктору родительского мы по-прежнему передаем два аргумента. Больше он и не принимает.
При создании объекта от класса NumMult надо передавать три аргумента:
val b = NumMult(1, 3, 2)
Первые два будут присвоены полям number и step и использоваться в функциях inc() и dec() . Третий будет присвоен свойству coefficient и использоваться только в методе mult() .
Теперь представим, что класс NumMult имеет два конструктора, а у NumInc он по прежнему один. В это случае вторичный конструктор NumMult должен делегировать к первичному своего же класса, а уже тот будет обращаться к конструктору родительского класса.
class NumMult(num: Int, gap: Int, coef: Int): NumInc(num, gap) { var coefficient = coef constructor() : this(0, 1, 2) fun mult() {number *= coefficient} }
Пример создания объекта через вторичный конструктор:
val c = NumMult()
Если у дочернего класса есть первичный конструктор, то все вторичные должны делегировать к нему. И только через него – к конструктору родительского класса. Однако если первичного конструктора нет, вторичные должны напрямую вызывать конструкторы родительского класса через ключевое слово super . Пример с двумя конструкторами как в основном, так и в дочернем классе при том, что в дочернем нет первичного:
open class NumInc(n: Int, gap: Int) { var number = n var step = gap constructor(): this(0, 1) fun inc() {number += step} fun dec() {number -= step} }
class NumMult: NumInc { var coefficient = 2 constructor(num: Int, gap: Int, coef: Int): super(num, gap) { coefficient = coef } constructor(): super() fun mult() {number *= coefficient} }
Обратите внимание, в данном случае один вторичный конструктор дочернего класса вызывает первичный родительского. Другой вторичный дочернего делегирует ко вторичному родительского. Определяется это количеством передаваемых аргументов.
Рассмотрим другое преимущество наследования в ООП. В Kotlin мы можем присвоить переменной более общего типа объект дочернего типа, а не только своего собственного.
val a: NumInc = NumInc(2, 1) val b: NumInc = NumMult(1, 3, 2)
Однако, поскольку переменная b имеет тип NumInc через нее нельзя получить доступ к свойствам и методам, которых нет в NumInc . Объект NumMult приводится к типу NumInc с потерей своих дополнительных свойств и методов.
С другой стороны, как мы узнаем из следующего урока, дочерние классы не всегда и не только отличаются от родительских расширением их возможностей, часто лишь переопределением. У дочернего класса могут быть почти такие же методы, как у родительского, но их программный код будет несколько иным.
Таким образом, объекты разных дочерних классов одного родительского, или дочернего и родительского, могут обладать одними и теми же методами. Это позволяет производить групповую обработку таких разноклассовых объектов. Например, мы можем создать список объектов NumInc и NumMult , дальше в цикле перебрать список, вызывая один и тот же метод для всех объектов.
fun main() { val a: ListNumInc> = listOf( NumMult(),NumMult(3,4,3), NumInc(10, 3), NumInc(5, 1)) for(i in a) { i.inc() println(i.number) } }
Результат выполнения программы:
1 7 13 6
Практическая работа:
Переделайте последний пример классов из урока так, чтобы родительский класс содержал только один конструктор, а дочерний – два.
X Скрыть Наверх
Введение в объектно-ориентированное программирование на Kotlin
Наследование в программировании: определение, виды, описание и отличия
Наследование классов в ОО П ( объектно-ориентированном программировании)
- родительские или базовые классы — это классы , от которых заимствовали характеристики и свойства;
- потомственные или наследующие классы — это классы, которые получились на основе характеристик родительских классов.
- простое,
- множественное.
Простое наследование классов в программировании
Простое наследование еще называет ся одиночным — это наслед ование , при котором создается «родство» между двумя классами. То ест ь о дин класс-потомок перенимает характеристики от одного родительского класса.
Даже если от одного родительского класса будет исходить несколько классов-потомков, это все равно будет прост ым наследование м . Простое наследование поддерживается большинством объектно-ориентированных языков, в отличие от множественного наслед ования .
Множественное наслед ование классов в программировании
Множественное наслед ование классов подразумевает ситуацию, когда один потомственный класс принимает характеристики от нескольких родительских классов. Такой подход помогает реализовывать очень гибкие и настраиваемые классы-потомки.
Его поддерживают не все ОО-языки. Например, множественное наслед ование поддерживают C, C++, Python, Eiffel, UML, но не поддерживают Java и C#. Некоторые языки отказались от множественного наслед ования , потому что оно является источником потенциальных ошибок из-за присутствия одинаковых имен у классов. Поэтому в таких языках было решено уйти от множественного наслед ования и заменить его интерфейсами.
Интерфейс — это подход в программировании, при котором определяются отношения между объектами, связанны ми похожими характеристиками. Такой подход довольно популярен в объектно-ориентированном программировании, так как исключает многие ошибки, возникающие в других подходах.
Наследование в программировании: реализация в языках
Давайте посмотрим , как реализуется наследование в некоторых языках программирования.
Наследование классов в С/С++ реализуется по следующему шаблону:
class Q <>; // Родительский класс
class W : public Q <>; // спецификатор Public
class E : protected Q <>; // спецификатор Protected
class R : private Q <>; // спецификатор Private
Как видно, в С/С++ наследование может быть организовано тремя спецификаторами. Данные спецификаторы объявляются в родительском и потомственном классе. Разный спецификатор — разные отношения между этими классами. Данной теме мы посвятим отдельную статью.
Множественное наследование на Python происходит по следующему шаблону:
class FirstParent(object): # первый родительский класс
def m1(self): pass
class SecondParent(object): # второй родительский класс
def m1(self): pass
class Inheritor(FirstParent, SecondParent): # Потомственный класс с двумя родительскими
В PHP реализация наследования происходит по следующему шаблону:
class Inheritor extends Parent
>
Важная особенность в PHP — родительский класс объявляется после потомственного класса, при этом обязательно использовать перед именем родительского класса ключево е слов о «extends».
Заключение
Наследование классов в программировании — это возможность повторно использовать уже написанный код для какого-либо класса. А это существенно экономит время на разработку и отладку программы.
Наследование классов — это процесс , связанный с объектно-ориентированным программированием, однако в разных ОО-языках он реализован по-разному. Например, множественное наследование поддерживают не все объектно-ориентированные языки.
Мы будем очень благодарны
если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.