stringtranslate.com

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

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

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

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

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

История

В 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++ 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]

class SubClass : видимость SuperClass { // члены подкласса };    

Некоторые языки также поддерживают наследование других конструкций. Например, в 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 . а  =  я  .б = б   def  Transform ( self ,  x ):  поднять  NotImplementedError  входы def ( self ):  диапазон возврата  ( self . a , self . b )  def  Compute ( self )  : вернуть  сумму ( self.transform ( value ) для значения в 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] Подтипирование позволяет заменить данный тип другим типом или абстракцией и, как говорят, устанавливает связь между подтипом и некоторой существующей абстракцией, явно или неявно, в зависимости от языковой поддержки. Отношения могут быть выражены явно через наследование в языках, которые поддерживают наследование как механизм подтипирования. Например, следующий код C++ устанавливает явные отношения наследования между классами B и A , где B является одновременно подклассом и подтипом A и может использоваться как A везде, где указан B (через ссылку, указатель или сам объект).

класс A { общественный : недействительный DoSomethingALike () const {} };       класс B : общественный A { общественный : недействительный DoSomethingBLike () const {} };          void UseAnA ( const A & a ) { a . СделатьЧто-нибудьAlike (); }     недействительный SomeFunc () { B b ; УсеАнА ( б ); // b можно заменить на A. }      

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

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

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

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

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

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

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

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

Наследование реализации вызывает споры среди программистов и теоретиков объектно-ориентированного программирования, по крайней мере, с 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. ^ Мэдсен, OL (1989). «Виртуальные классы: мощный механизм объектно-ориентированного программирования». Материалы конференции «Объектно-ориентированные системы, языки и приложения программирования — OOPSLA '89» . стр. 397–406. дои : 10.1145/74877.74919. ISBN 0897913337. S2CID  1104130.
  3. ^ Дэвис, Терк (2021). Передовые методы и глубокое обучение в компьютерном зрении . Эльзевир Наука. стр. 179–342.
  4. ^ аб Кук, Уильям Р.; Хилл, Уолтер; Каннинг, Питер С. (1990). Наследование не является подтипом . Материалы 17-го симпозиума ACM SIGPLAN-SIGACT по принципам языков программирования (POPL). стр. 125–135. CiteSeerX 10.1.1.102.8635 . дои : 10.1145/96709.96721. ISBN  0-89791-343-4.
  5. ^ Карделли, Лука (1993). Типовое программирование (Технический отчет). Корпорация цифрового оборудования . п. 32–33. Отчет об исследовании SRC 45.
  6. ^ abc Михайлов, Леонид; Секерински, Эмиль (1998). Исследование проблемы хрупкого базового класса (PDF) . Материалы 12-й Европейской конференции по объектно-ориентированному программированию (ECOOP). Конспекты лекций по информатике. Том. 1445. Спрингер. стр. 355–382. дои : 10.1007/BFb0054099. ISBN 978-3-540-64737-9. Архивировано из оригинала (PDF) 13 августа 2017 г. Проверено 28 августа 2015 г.
  7. ^ Темперо, Юэн; Ян, Хун Юл; Ноубл, Джеймс (2013). Что программисты делают с наследованием в Java (PDF) . ЭКООП 2013–Объектно-ориентированное программирование. Конспекты лекций по информатике. Том. 7920. Спрингер. стр. 577–601. дои : 10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.
  8. ^ Хоар, АВТОМОБИЛЬ (1966). Обработка записей (PDF) (Технический отчет). стр. 15–16.
  9. ^ Даль, Оле-Йохан; Найгаард, Кристен (май 1967 г.). Объявления классов и подклассов (PDF) . Рабочая конференция ИФИП по языкам моделирования. Осло: Норвежский вычислительный центр.
  10. ^ Даль, Оле-Йохан (2004). «Рождение объектной ориентации: языки моделирования» (PDF) . Конспекты лекций по информатике. Том. 2635. стр. 15–25. дои : 10.1007/978-3-540-39993-3_3. ISBN 978-3-540-21366-6. {{cite book}}: |journal=игнорируется ( помощь ) ; Отсутствует или пусто |title=( помощь )
  11. ^ «Наследование C++». www.cs.nmsu.edu .
  12. ^ Страуструп, Бьярн (1994). Проектирование и эволюция C++ . Пирсон. п. 417. ИСБН 9780135229477.
  13. ^ Шильдт, Герберт (2003). Полный справочник по C++ . Тата МакГроу Хилл. п. 417. ИСБН 978-0-07-053246-5.
  14. ^ Балагурусами, Э. (2010). Объектно-ориентированное программирование на C++ . Тата МакГроу Хилл. п. 213. ИСБН 978-0-07-066907-9.
  15. ^ переопределить (Справочник по C#)
  16. ^ «GotW # 60: Проектирование классов, безопасных для исключений, Часть 2: Наследование» . Gotw.ca.Проверено 15 августа 2012 г.
  17. ^ Венугопал, КР; Буя, Раджкумар (2013). Освоение С++ . Тата МакГроу Хилл Образовательная Частная Лимитед. п. 609. ИСБН 9781259029943.
  18. ^ Митчелл, Джон (2002). «10 «Концепции объектно-ориентированных языков»". Концепции в языке программирования . Cambridge University Press. стр. 287. ISBN 978-0-521-78098-8.
  19. ^ abc Голуб, Аллен (1 августа 2003 г.). «Почему расширяется – это зло» . Проверено 10 марта 2015 г.
  20. ^ Зейтер, Линда М.; Палсберг, Йенс; Либерхерр, Карл Дж. (1996). «Эволюция поведения объектов с использованием контекстных отношений». Заметки по разработке программного обеспечения ACM SIGSOFT . 21 (6): 46. CiteSeerX 10.1.1.36.5053 . дои : 10.1145/250707.239108. 
  21. ^ Америка, Пьер (1991). Проектирование объектно-ориентированного языка программирования с поведенческим подтипированием. Школа/семинар REX по основам объектно-ориентированных языков. Конспекты лекций по информатике. Том. 489. стр. 60–90. дои : 10.1007/BFb0019440. ISBN 978-3-540-53931-5.

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