В объектно-ориентированном программировании наследование — это механизм создания объекта или класса на основе другого объекта ( наследование на основе прототипа ) или класса ( наследование на основе классов ) с сохранением аналогичной реализации . Также определяется как получение новых классов (подклассов) из существующих, таких как суперкласс или базовый класс , а затем формирование их в иерархию классов. В большинстве объектно-ориентированных языков на основе классов, таких как 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]
// Класс реализации языка C++ A { ... }; // Базовый класс class B : public A { ... }; // B является производным от A class C : public B { ... }; // C получен из B
Подклассы , производные классы , классы-наследники или дочерние классы — это модульные производные классы, которые наследуют одну или несколько языковых сущностей от одного или нескольких других классов (называемых суперклассом , базовыми классами или родительскими классами ). Семантика наследования классов варьируется от языка к языку, но обычно подкласс автоматически наследует переменные экземпляра и функции-члены своих суперклассов.
Общая форма определения производного класса такова: [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 с именем « Сотрудник» , который содержит название должности, работодателя и зарплату человека.
При определении этой иерархии наследования мы уже определили некоторые ограничения, не все из которых желательны:
Принцип составного повторного использования является альтернативой наследованию. Этот метод поддерживает полиморфизм и повторное использование кода, отделяя поведение от основной иерархии классов и включая определенные классы поведения, необходимые в любом классе бизнес-домена. Этот подход позволяет избежать статичной природы иерархии классов, позволяя изменять поведение во время выполнения, и позволяет одному классу реализовывать поведение в виде шведского стола, а не ограничиваться поведением своих классов-предков.
Наследование реализации вызывает споры среди программистов и теоретиков объектно-ориентированного программирования, по крайней мере, с 1990-х годов. Среди них авторы Design Patterns , которые вместо этого выступают за наследование интерфейсов и предпочитают композицию наследованию. Например, шаблон декоратора (как упоминалось выше) был предложен для преодоления статической природы наследования между классами. В качестве более фундаментального решения той же проблемы ролевое программирование вводит особые отношения, объединяющие свойства наследования и композиции в новую концепцию. [ нужна цитата ]
По словам Аллена Холуба , основная проблема наследования реализации заключается в том, что оно вводит ненужную связь в форме «проблемы хрупкого базового класса» : [6] модификации реализации базового класса могут вызвать непреднамеренные изменения поведения в подклассах. Использование интерфейсов позволяет избежать этой проблемы, поскольку не используется общая реализация, а только API. [19] Другой способ выразить это так: «наследование нарушает инкапсуляцию ». [20] Проблема явно проявляется в открытых объектно-ориентированных системах, таких как фреймворки , где ожидается, что клиентский код будет наследовать от классов, предоставляемых системой, а затем заменять классы системы в ее алгоритмах. [6]
Сообщается, что изобретатель Java Джеймс Гослинг выступил против наследования реализации, заявив, что он не стал бы включать его, если бы перепроектировал Java. [19] Языковые конструкции, отделяющие наследование от подтипов (наследование интерфейса), появились еще в 1990 году; [21] Современный пример — язык программирования Go .
Сложное наследование или наследование, используемое в недостаточно зрелом проекте, может привести к проблеме йо-йо . Когда в конце 1990-х годов наследование использовалось в качестве основного подхода к структурированию программ, разработчики имели тенденцию разбивать код на большее количество уровней наследования по мере роста функциональности системы. Если команда разработчиков объединила несколько уровней наследования с принципом единой ответственности, это привело к появлению множества очень тонких слоев кода, многие из которых состояли всего из 1 или 2 строк реального кода. [ нужна цитата ] Слишком большое количество уровней усложняет отладку, поскольку становится трудно определить, какой уровень необходимо отлаживать.
Другая проблема с наследованием заключается в том, что подклассы должны быть определены в коде, а это означает, что пользователи программы не могут добавлять новые подклассы во время выполнения. Другие шаблоны проектирования (например, Entity-Component-system ) позволяют пользователям программы определять варианты сущности во время выполнения.
{{cite book}}
: |journal=
игнорируется ( помощь ) ; Отсутствует или пусто |title=
( помощь )