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

редактировать
Механизм создания объекта или класса на основе другого объекта или класса с аналогичной реализацией

В объектно-ориентированном программировании, наследование - это механизм базирования объекта или класса на другом объекте (на основе прототипа наследование ) или класс (наследование на основе классов ), сохраняя аналогичную реализацию. Также определяется как создание новых классов (подклассов) из существующих, таких как суперкласс или базовый класс, с последующим формированием их в иерархию классов. В большинстве объектно-ориентированных языков на основе классов объект, созданный посредством наследования, «дочерний объект», приобретает все свойства и поведение «родительского объекта», за исключением: конструкторов, деструктора, перегруженные операторы и дружественные функции базового класса. Наследование позволяет программистам создавать классы, основанные на существующих классах, определять новую реализацию с сохранением того же поведения (реализация интерфейса ), повторно использовать код и независимо расширять исходное программное обеспечение через общедоступные классы и интерфейсы. Отношения объектов или классов через наследование приводят к ориентированному графу.

Наследование было изобретено в 1969 году для Simula и теперь используется во многих объектно-ориентированных языках программирования, таких как Java., C ++ или Python.

Унаследованный класс называется подклассом его родительского класса или суперкласса. Термин «наследование» в общих чертах используется как для программирования на основе классов, так и на основе прототипов, но в узком смысле термин зарезервирован для программирования на основе классов (один класс наследуется от другого), при этом соответствующая техника в программировании на основе прототипов является вместо этого вызывается делегирование (один объект делегирует другому).

Наследование не следует путать с подтипом. В некоторых языках наследование и подтипы совпадают, тогда как в других они различаются; в общем, подтип устанавливает отношение is-a, тогда как наследование только повторно использует реализацию и устанавливает синтаксические отношения, не обязательно семантические отношения (наследование не гарантирует поведенческого подтипа ). Чтобы различать эти концепции, создание подтипов также известно как наследование интерфейсов, тогда как наследование, как определено здесь, известно как наследование реализации или наследование кода. Тем не менее, наследование - это широко используемый механизм для установления отношений подтипов.

Наследование контрастирует с композицией объекта, где один объект содержит другой объект (или объекты одного класса содержат объекты другого класса) ; см. композиция вместо наследования. Композиция реализует отношение has-a, в отличие от отношения is-a для подтипа.

Содержание

  • 1 Типы
  • 2 Подклассы и суперклассы
    • 2.1 Неклассифицированные классы
    • 2.2 Непереопределяемые методы
    • 2.3 Виртуальные методы
  • 3 Видимость унаследованных членов
  • 4 Приложения
    • 4.1 Переопределение
    • 4.2 Повторное использование кода
  • 5 Наследование и создание подтипов
    • 5.1 Ограничения конструкции
  • 6 Проблемы и альтернативы
  • 7 См. Также
  • 8 Примечания
  • 9 Ссылки
  • 10 Дополнительная литература

Типы

Одиночное наследование Множественное наследование

Существуют различные типы наследования в зависимости от парадигмы и конкретного языка.

Одиночное наследование

, при котором подклассы наследуют особенности одного суперкласс. Один класс приобретает свойства другого класса.

Множественное наследование

, при котором один класс может иметь более одного суперкласса и наследовать функции от всех родительских классов.

«Множественное наследование... считалось очень сложным для эффективной реализации. Например, в кратком изложении C ++ в его книге по Objective C, Брэд Кокс на самом деле утверждал, что добавление множественного наследования в C ++ невозможно. Таким образом, множественное наследование казалось более сложной задачей. Поскольку я рассматривал множественное наследование еще в 1982 году и нашел простой и эффективный метод реализации в 1984 году, я не мог: Я думаю, это единственный случай, когда мода повлияла на последовательность событий. "

Бьярн Страуструп
Многоуровневое наследование

, когда подкласс наследуется от другого подкласса. Нередко класс является производным от другого производного класса, как показано на рисунке «Многоуровневое наследование».

Многоуровневое наследование

Класс A служит базовым классом для производного класса B, который, в свою очередь, служит базовый класс для производного класса C. Класс B известен как промежуточный базовый класс, поскольку он обеспечивает связь для наследования между A и C. Цепочка ABC известна как путь наследования.

Производный класс с многоуровневым наследованием объявляется следующим образом:

Class A (...); // Базовый класс Class B: public A (...); // B производный от A Class C: public B (...); // C, производный от B

Этот процесс можно расширить до любого количества уровней.

Иерархическое наследование

Здесь один класс служит суперклассом (базовым классом) для более чем одного подкласса. Например, родительский класс A может иметь два подкласса B и C. Родительский класс B и C - это A, но B и C - это два отдельных подкласса.

Гибридное наследование

Гибридное наследование - это когда происходит сочетание двух или более из вышеуказанных типов наследования. Примером этого является случай, когда у класса A есть подкласс B, который имеет два подкласса, C и D. Это смесь как многоуровневого, так и иерархического наследования.

Подклассы и суперклассы

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

Общая форма определения производного класса:

class SubClass: visibility SuperClass {// члены подкласса};
  • Двоеточие указывает, что подкласс наследуется от суперкласса. Видимость не является обязательной и, если есть, может быть частной или публичной. По умолчанию видимость закрыта. Видимость указывает, являются ли функции базового класса производными от частного лица или открытыми.

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

Неклассифицированные классы

В некоторых языках класс может быть объявлен как неподклассифицированный путем добавления определенного в объявление класса. Примеры включают ключевое слово finalв Java и C ++ 11 и далее или ключевое слово sealedв C #. Такие модификаторы добавляются к объявлению класса перед ключевым словом classи объявлением идентификатора класса. Такие неподклассифицируемые классы ограничивают возможность повторного использования, особенно когда разработчики имеют доступ только к предварительно скомпилированным двоичным файлам, а не к исходному коду.

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

Непереопределяемые методы

Так же, как классы могут быть неподклассифицируемыми, объявления методов могут содержать модификаторы методов, которые предотвращают переопределение метода (т. Е. Замену новой функцией с тем же именем и подпись типа в подклассе). Метод private нельзя переопределить просто потому, что он недоступен для других классов, кроме класса, функцией-членом которого он является (однако это неверно для C ++ ). Метод finalв Java, запечатанный методв C # или замороженная функцияв Eiffel нельзя переопределить.

Виртуальные методы

Если метод суперкласса является виртуальным методом, то вызовы метода суперкласса будут динамически отправляться. Некоторые языки требуют, чтобы методы были специально объявлены как виртуальные (например, C ++ ), а в других все методы являются виртуальными (например, Java ). Вызов не виртуального метода всегда будет отправляться статически (т.е.адрес вызова функции определяется во время компиляции). Статическая отправка выполняется быстрее, чем динамическая, и допускает такие оптимизации, как встроенное расширение.

Видимость унаследованных членов

В следующей таблице показано, какие переменные и функции наследуются в зависимости от видимости, заданной при создании класса.

Видимость базового классаВидимость производного класса
Открытая производнаяЧастная производнаяЗащищенная производная
  • Закрытая →
  • Защищенная →
  • Public →
  • Не унаследовано
  • Защищено
  • Public
  • Не унаследовано
  • Private
  • Private
  • Не унаследовано
  • Защищенные
  • Защищенные

Приложения

Наследование используется для совместного связывания двух или более классов друг с другом.

Переопределение

Иллюстрация переопределения метода

Многие объектно-ориентированные языки программирования позволяют классу или объекту заменять реализацию аспекта - обычно поведения, - который он унаследовал. Этот процесс называется переопределением. При переопределении возникает сложность: какую версию поведения использует экземпляр унаследованного класса - ту, которая является частью его собственного класса, или версию родительского (базового) класса? Ответ зависит от языка программирования, и некоторые языки предоставляют возможность указать, что конкретное поведение не должно быть отменено и должно вести себя так, как определено базовым классом. Например, в C # базовый метод или свойство можно переопределить в подклассе, только если он помечен модификатором virtual, abstract или override, тогда как в языках программирования, таких как Java, могут использоваться различные методы. вызывается для переопределения других методов. Альтернативой переопределению является скрытие унаследованного кода.

Повторное использование кода

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

В следующем примере Python подклассы SquareSumComputer и CubeSumComputer переопределяют метод transform () базового класса SumComputer. Базовый класс содержит операции по вычислению суммы квадратов между двумя целыми числами. Подкласс повторно использует все функции базового класса, за исключением операции, которая преобразует число в его квадрат, заменяя его операцией, которая преобразует число в его квадрат и куб. соответственно. Таким образом, подклассы вычисляют сумму квадратов / кубиков между двумя целыми числами.

Ниже приведен пример Python.

класс SumComputer (объект): def __init __ (self, a, b): self.a = a self.b = b def transform (self, x): поднять NotImplementedError def inputs (self): return range (self. a, self.b) def compute (self): return sum (self.transform (value) for value in self.inputs ()) class SquareSumComputer (SumComputer): def transform (self, x): return x * x class CubeSumComputer (SumComputer): def transform (self, x): return x * x * x

В большинстве случаев наследование классов с единственной целью повторного использования кода вышло из моды. Основная проблема заключается в том, что наследование реализации не обеспечивает никакой гарантии полиморфной заменяемости - экземпляр повторно используемого класса не обязательно может быть заменен экземпляром унаследованного класса. Альтернативный метод, явное делегирование, требует дополнительных усилий по программированию, но позволяет избежать проблемы заменяемости. В C ++ частное наследование может использоваться как форма наследования реализации без возможности замены. В то время как публичное наследование представляет собой отношение «is-a », а делегирование представляет собой отношение «has-a », частное (и защищенное) наследование можно рассматривать как «реализованное в термины "отношения".

Еще одно частое использование наследования - гарантия того, что классы поддерживают определенный общий интерфейс; то есть они реализуют одни и те же методы. Родительский класс может быть комбинацией реализованных операций и операций, которые должны быть реализованы в дочерних классах. Часто нет изменений интерфейса между супертипом и подтипом - дочерний класс реализует описанное поведение вместо своего родительского класса.

Наследование против подтипов

Наследование похоже на , но отличается от него. подтип. Подтипирование позволяет заменить данный тип другим типом или абстракцией и, как говорят, устанавливает связь is-a между подтипом и некоторой существующей абстракцией, явно или неявно, в зависимости от поддержки языка. Отношения могут быть явно выражены через наследование на языках, которые поддерживают наследование как механизм подтипа. Например, следующий код C ++ устанавливает явное отношение наследования между классами B и A, где B является как подклассом, так и подтипом A, и может использоваться как A везде, где указан B (через ссылку, указатель или сам объект).

класс A {public: void DoSomethingALike () const {}}; класс B: общественность A {public: void DoSomethingBLike () const {}}; void UseAnA (const A a) {a.DoSomethingALike (); } void SomeFunc () {B b; UseAnA (b); // b можно заменить на A.}

В языках программирования, которые не поддерживают наследование в качестве механизма подтипов, связь между базовым классом и производным классом - это только связь между реализациями ( механизм для повторного использования кода) по сравнению с отношениями между типами. Наследование, даже в языках программирования, которые поддерживают наследование как механизм подтипов, не обязательно влечет за собой поведенческое подтипирование. Вполне возможно создать класс, объект которого будет вести себя некорректно при использовании в контексте, где ожидается родительский класс; см. принцип замещения Лискова. (Сравните коннотацию / обозначение.) В некоторых языках ООП понятия повторного использования кода и подтипа совпадают, потому что единственный способ объявить подтип - это определить новый класс, который наследует реализацию другого.

Ограничения проектирования

Широкое использование наследования при разработке программы накладывает определенные ограничения.

Например, рассмотрим класс Person, который содержит имя человека, дату рождения, адрес и номер телефона. Мы можем определить подкласс Person с именем Student, который содержит средний балл человека и пройденные классы, и другой подкласс Person с именем Employee, который содержит должность человека, работодателя и зарплата.

При определении этой иерархии наследования мы уже определили некоторые ограничения, не все из которых желательны:

Единственность
Используя одиночное наследование, подкласс может наследовать только от одного суперкласса. Продолжая приведенный выше пример, Человек может быть либо Студентом, либо Сотрудником, но не обоими сразу. Использование множественного наследования частично решает эту проблему, так как затем можно определить класс StudentEmployee, который наследуется как от Student, так и от Employee. Однако в большинстве реализаций он по-прежнему может наследовать от каждого суперкласса только один раз и, таким образом, не поддерживает случаи, когда студент имеет две работы или посещает два учреждения. Модель наследования, доступная в Eiffel, делает это возможным благодаря поддержке повторного наследования.
Static
Иерархия наследования объекта фиксируется в экземпляре, когда тип объекта выбран и не меняется со временем. Например, граф наследования не позволяет объекту Student стать объектом Employee, сохраняя при этом состояние его суперкласса Person. (Такое поведение, однако, может быть достигнуто с помощью шаблона декоратора .) Некоторые критиковали наследование, утверждая, что оно ограничивает разработчиков их исходными стандартами проектирования.
Видимость
Всякий раз, когда клиентский код имеет доступ к объекту, обычно он имеет доступ ко всем данным суперкласса объекта. Даже если суперкласс не был объявлен общедоступным, клиент все равно может привести объект к его типу суперкласса. Например, невозможно дать функции указатель на средний балл ученика и стенограмму, не предоставив этой функции доступа ко всем личным данным, хранящимся в суперклассе Person ученика. Многие современные языки, включая C ++ и Java, предоставляют "защищенный" модификатор доступа, который позволяет подклассам получать доступ к данным, не разрешая доступ к ним какому-либо коду вне цепочки наследования.

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

Проблемы и альтернативы

Наследование реализации вызывает споры среди программистов и теоретиков объектно-ориентированного программирования, по крайней мере, с 1990-х годов. Среди них есть авторы Design Patterns, которые вместо этого выступают за наследование интерфейсов и предпочитают композицию наследованию. Например, шаблон декоратора (как упоминалось выше) был предложен для преодоления статической природы наследования между классами. В качестве более фундаментального решения той же проблемы ролевое программирование вводит отчетливую взаимосвязь, в которой сочетаются свойства наследования и композиции в новую концепцию.

Согласно Аллен Холуб, основная проблема с наследованием реализации заключается в том, что оно вводит ненужную связь в виде «проблемы хрупкого базового класса» : изменения в реализации базового класса могут вызывают непреднамеренные поведенческие изменения в подклассах. Использование интерфейсов позволяет избежать этой проблемы, потому что нет общей реализации, только API. Другой способ заявить об этом - «наследование нарушает инкапсуляцию ». Проблема четко проявляется в открытых объектно-ориентированных системах, таких как frameworks, где ожидается, что клиентский код будет наследовать от классов, предоставляемых системой, а затем заменять классы системы в своих алгоритмах.

По сообщениям, Изобретатель Java Джеймс Гослинг высказался против наследования реализации, заявив, что он не включил бы его, если бы он перепроектировал Java. Языковые конструкции, которые отделяют наследование от подтипов (наследование интерфейса), появились еще в 1990 году; Современный пример этого - язык программирования Go.

Сложное наследование или наследование, используемое в недостаточно зрелом дизайне, может привести к проблеме йо-йо. Когда в конце 1990-х годов наследование использовалось в качестве основного подхода к структурированию кода в системе, разработчики, естественно, начали разбивать код на несколько уровней наследования по мере роста функциональности системы. Если команда разработчиков объединила несколько уровней наследования с принципом единой ответственности, она создала много супертонких слоев кода, многие из которых имели бы только 1 или 2 строки кода на каждом уровне. До того, как команды на собственном горьком опыте узнали, что 2 или 3 уровня являются оптимальным числом уровней, уравновешивающим преимущество повторного использования кода с увеличением сложности с каждым уровнем, не было ничего необычного в работе над фреймворками наследования с 10 и до 30 уровней. Например, 30 уровней сделали отладку серьезной проблемой, просто чтобы знать, какой уровень нужно отлаживать. PowerBuilder создал одну из лучших библиотек кода, которая в основном использовала наследование, она была построена с 3-4 уровнями. Количество слоев в библиотеке наследования имеет решающее значение и должно составлять не более 4 слоев, иначе библиотека станет слишком сложной и требует много времени для использования.

Еще одна проблема с наследованием заключается в том, что подклассы должны быть определены в коде, а это означает, что пользователи программы не могут добавлять новые подклассы. Другие шаблоны проектирования (такие как Entity – component – ​​system ) позволяют пользователям программы определять варианты объекта во время выполнения.

См. Также

Примечания

Ссылки

Дополнительная литература

Последняя правка сделана 2021-05-24 14:58:37
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте