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