Что такое наследование в объектно ориентированном программировании
Перейти к содержимому

Что такое наследование в объектно ориентированном программировании

  • автор:

Наследование и ещё немного полиморфизма: 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} }

В заголовке после параметров первичного конструктора (если он есть) ставится двоеточие, после которого идет имя родительского класса. Поскольку конструктор родительского класса предусматривает два параметра, мы должны их туда передать.

Наследование в Kotlin - пример родительского и дочернего классов

После этого объекты 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

Наследование в программировании: определение, виды, описание и отличия

Lorem ipsum dolor

Наследование классов в ОО П ( объектно-ориентированном программировании)

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

Простое наследование классов в программировании

Простое наследование еще называет ся одиночным — это наслед ование , при котором создается «родство» между двумя классами. То ест ь о дин класс-потомок перенимает характеристики от одного родительского класса.

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

Множественное наслед ование классов в программировании

Множественное наслед ование классов подразумевает ситуацию, когда один потомственный класс принимает характеристики от нескольких родительских классов. Такой подход помогает реализовывать очень гибкие и настраиваемые классы-потомки.

Его поддерживают не все ОО-языки. Например, множественное наслед ование поддерживают 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».

Заключение

Наследование классов в программировании — это возможность повторно использовать уже написанный код для какого-либо класса. А это существенно экономит время на разработку и отладку программы.

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

Мы будем очень благодарны

если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *