stringtranslate.com

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

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

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

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

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

История

В 1966 году Тони Хоар представил некоторые замечания о записях, и в частности, идею подклассов записей, типов записей с общими свойствами, но различающихся по тегу варианта и имеющих поля, закрытые для варианта. [8] Под влиянием этого в 1967 году Оле-Йохан Даль и Кристен Нигаард представили дизайн, который позволял указывать объекты, принадлежащие к разным классам, но имеющие общие свойства. Общие свойства были собраны в суперклассе, и каждый суперкласс сам мог потенциально иметь суперкласс. Таким образом, значения подкласса были составными объектами, состоящими из некоторого количества префиксных частей, принадлежащих различным суперклассам, плюс основная часть, принадлежащая подклассу. Все эти части были объединены вместе. [9] Атрибуты составного объекта были бы доступны с помощью точечной нотации. Эта идея была впервые принята в языке программирования Simula 67. [10] Затем эта идея распространилась на Smalltalk , C++ , Java , Python и многие другие языки.

Типы

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

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

Единичное наследование
где подклассы наследуют черты одного суперкласса. Класс приобретает свойства другого класса.
Множественное наследование
где один класс может иметь более одного суперкласса и наследовать функции от всех родительских классов.

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

Многоуровневое наследование
где подкласс наследуется от другого подкласса. Нередко класс выводится из другого производного класса, как показано на рисунке «Многоуровневое наследование».
Многоуровневое наследование
Класс A служит базовым классом для производного класса B , который, в свою очередь, служит базовым классом для производного класса C. Класс B известен как промежуточный базовый класс, поскольку он обеспечивает связь для наследования между A и C. Цепочка ABC известна как путь наследования .
Производный класс с многоуровневым наследованием объявляется следующим образом:
// Реализация языка C++ 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. Это смесь как многоуровневого наследования, так и иерархического наследования.

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

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

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

класс Подкласс : видимость Суперкласс { // члены подкласса };    

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

Неподклассифицируемые классы

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

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

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

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

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

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

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

В следующей таблице показано, какие переменные и функции наследуются в зависимости от видимости, заданной при создании класса, с использованием терминологии, установленной в C++. [14]

Приложения

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

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

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

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

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

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

В следующем примере 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 )  для  value  in  self . inputs ())класс  SquareSumComputer ( SumComputer ):  def  transform ( self ,  x ):  return  x  *  xкласс  CubeSumComputer ( SumComputer ):  def  transform ( self ,  x ):  return  x  *  x  *  x

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

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

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

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

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

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

Ограничения дизайна

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

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

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

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

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

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

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

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

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

Сложное наследование или наследование, используемое в недостаточно зрелом дизайне, может привести к проблеме йо-йо . Когда наследование использовалось в качестве основного подхода к структурированию программ в конце 1990-х годов, разработчики имели тенденцию разбивать код на большее количество слоев наследования по мере роста функциональности системы. Если команда разработчиков объединяла несколько слоев наследования с принципом единой ответственности, это приводило к появлению множества очень тонких слоев кода, многие из которых состояли всего из 1 или 2 строк фактического кода. [ необходима цитата ] Слишком много слоев делают отладку значительной проблемой, поскольку становится трудно определить, какой слой нужно отлаживать.

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

Смотрите также

Примечания

  1. ^ Это, как правило, справедливо только для статически типизированных объектно-ориентированных языков на основе классов, таких как C++ , C# , Java и Scala .

Ссылки

  1. ^ Джонсон, Ральф (26 августа 1991 г.). «Проектирование повторно используемых классов» (PDF) . www.cse.msu.edu .
  2. ^ Madsen, OL (1989). "Виртуальные классы: мощный механизм в объектно-ориентированном программировании". Труды конференции по объектно-ориентированным системам программирования, языкам и приложениям - OOPSLA '89 . стр. 397–406. doi :10.1145/74877.74919. ISBN 0897913337. S2CID  1104130.
  3. ^ Дэвис, Тёрк (2021). Передовые методы и глубокое обучение в компьютерном зрении . Elsevier Science. С. 179–342.
  4. ^ ab Кук, Уильям Р.; Хилл, Уолтер; Каннинг, Питер С. (1990). Наследование — это не подтипирование . Труды 17-го симпозиума ACM SIGPLAN-SIGACT по принципам языков программирования (POPL). стр. 125–135. CiteSeerX 10.1.1.102.8635 . doi :10.1145/96709.96721. ISBN  0-89791-343-4.
  5. ^ Карделли, Лука (1993). Typeful Programming (технический отчет). Digital Equipment Corporation . стр. 32–33. SRC Research Report 45.
  6. ^ abc Михайлов, Леонид; Секеринский, Эмиль (1998). Исследование проблемы хрупкого базового класса (PDF) . Труды 12-й Европейской конференции по объектно-ориентированному программированию (ECOOP). Конспект лекций по информатике. Том 1445. Springer. С. 355–382. doi :10.1007/BFb0054099. ISBN 978-3-540-64737-9. Архивировано из оригинала (PDF) 2017-08-13 . Получено 2015-08-28 .
  7. ^ Темперо, Эван; Янг, Хонг Юл; Нобл, Джеймс (2013). Что программисты делают с наследованием в Java (PDF) . ECOOP 2013–Объектно-ориентированное программирование. Конспект лекций по информатике. Том 7920. Springer. С. 577–601. doi :10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.
  8. ^ Хоар, КАР (1966). Обработка записей (PDF) (Технический отчет). стр. 15–16.
  9. ^ Даль, Оле-Йохан ; Нюгаард, Кристен (май 1967). Объявления классов и подклассов (PDF) . Рабочая конференция IFIP по языкам моделирования. Осло: Норвежский вычислительный центр.
  10. ^ Даль, Оле-Йохан (2004). "Рождение объектной ориентации: языки Simula" (PDF) . От объектной ориентации к формальным методам . Конспект лекций по информатике. Том 2635. С. 15–25. doi :10.1007/978-3-540-39993-3_3. ISBN 978-3-540-21366-6.
  11. ^ "C++ Inheritance". www.cs.nmsu.edu . Архивировано из оригинала 2023-09-24 . Получено 2018-05-16 .
  12. ^ Страуструп, Бьярне (1994). Проектирование и эволюция C++ . Пирсон. стр. 417. ISBN 9780135229477.
  13. ^ Schildt, Herbert (2003). Полный справочник C++ . Tata McGraw Hill. стр. 417. ISBN 978-0-07-053246-5.
  14. ^ Балагурусами, Э. (2010). Объектно-ориентированное программирование на C++ . Tata McGraw Hill. стр. 213. ISBN 978-0-07-066907-9.
  15. ^ переопределение (Справочник по C#)
  16. ^ "GotW #60: Проектирование классов, безопасных в отношении исключений, часть 2: Наследование". Gotw.ca . Получено 15 августа 2012 г.
  17. ^ Венугопал, КР; Буя, Раджкумар (2013). Освоение С++ . Тата МакГроу Хилл Образовательная Частная Лимитед. п. 609. ИСБН 9781259029943.
  18. ^ Митчелл, Джон (2002). "10 "Концепции в объектно-ориентированных языках"". Концепции в языке программирования . Издательство Кембриджского университета. стр. 287. ISBN 978-0-521-78098-8.
  19. ^ abc Holub, Allen (1 августа 2003 г.). "Почему extends is evil". Архивировано из оригинала 24 февраля 2019 г. Получено 10 марта 2015 г.
  20. ^ Seiter, Linda M.; Palsberg, Jens; Lieberherr, Karl J. (1996). «Эволюция поведения объектов с использованием контекстных отношений». ACM SIGSOFT Software Engineering Notes . 21 (6): 46. CiteSeerX 10.1.1.36.5053 . doi :10.1145/250707.239108. 
  21. ^ Америка, Пьер (1991). Разработка объектно-ориентированного языка программирования с поведенческим подтипированием. Школа/семинар REX по основам объектно-ориентированных языков. Конспект лекций по информатике. Том 489. С. 60–90. doi :10.1007/BFb0019440. ISBN 978-3-540-53931-5.

Дальнейшее чтение