Множественное наследование — это особенность некоторых объектно-ориентированных языков программирования , в которых объект или класс может наследовать функции более чем от одного родительского объекта или родительского класса . Оно отличается от одиночного наследования, где объект или класс может наследовать только от одного конкретного объекта или класса.
Множественное наследование было спорным вопросом в течение многих лет, [1] [2] при этом оппоненты указывали на его повышенную сложность и двусмысленность в таких ситуациях, как «проблема ромба», где может быть неясно, к какому родительскому классу принадлежит конкретная функция. наследуется, если указанную функцию реализует более одного родительского класса. Эту проблему можно решить разными способами, в том числе с помощью виртуального наследования . [3] Для устранения двусмысленности также были предложены альтернативные методы композиции объектов, не основанные на наследовании, такие как миксины и черты .
В объектно-ориентированном программировании (ООП) наследование описывает отношения между двумя классами, в которых один класс ( дочерний класс) является подклассом родительского класса . Дочерний элемент наследует методы и атрибуты родительского элемента, что позволяет использовать общие функции. Например, можно создать переменный класс Mammal с такими функциями, как поедание, размножение и т. д.; затем определите дочерний класс Cat , который наследует эти функции без необходимости их явного программирования, а также добавляет новые функции, такие как погоня за мышами .
Множественное наследование позволяет программистам использовать более одной полностью ортогональной иерархии одновременно, например, позволяя Cat наследовать от персонажа мультфильма , домашнего животного и млекопитающего и получать доступ к функциям всех этих классов.
Языки, поддерживающие множественное наследование, включают: C++ , Common Lisp (через объектную систему Common Lisp (CLOS)), EuLisp (через объектную систему EuLisp TELOS), Curl , Dylan , Eiffel , Logtalk , Object REXX , Scala (с помощью классов- миксинов ). ), OCaml , Perl , POP-11 , Python , R , Raku и Tcl (встроено в версии 8.6 или через Incremental Tcl ( Incr Tcl ) в более ранних версиях [4] [5] ).
Среда выполнения IBM System Object Model (SOM) поддерживает множественное наследование, и любой язык программирования, предназначенный для SOM, может реализовать новые классы SOM, унаследованные от нескольких баз.
Некоторые объектно-ориентированные языки, такие как Swift , Java , Fortran начиная с версии 2003 года , C# и Ruby реализуют одиночное наследование , хотя протоколы или интерфейсы предоставляют некоторые функции настоящего множественного наследования.
PHP использует классы признаков для наследования конкретных реализаций методов. Ruby использует модули для наследования нескольких методов.
« Проблема алмаза » (иногда называемая «Смертельным алмазом смерти» [6] ) — это неоднозначность, возникающая, когда два класса B и C наследуются от A, а класс D наследуется как от B, так и от C. Если существует метод в A, который B и C переопределили , а D не переопределяет его, то какую версию метода наследует D: версию B или версию C?
Например, в контексте разработки программного обеспечения с графическим пользовательским интерфейсом класс может наследовать как классы (для внешнего вида), так и (для функциональности/обработки ввода), а также классы , и оба наследуются от класса. Теперь, если метод вызывается для объекта и в классе нет такого метода , но есть переопределенный метод в или (или в обоих), какой метод в конечном итоге следует вызвать?Button
Rectangle
Clickable
Rectangle
Clickable
Object
equals
Button
Button
equals
Rectangle
Clickable
Ее называют «проблемой ромба» из-за формы диаграммы наследования классов в этой ситуации. В этом случае класс A находится вверху, B и C по отдельности под ним, а D объединяет их внизу, образуя ромбовидную форму.
Языки по-разному решают проблемы повторного наследования.
A
, реализующий интерфейсы Ia
и Ib
аналогичные методы, имеющие реализации по умолчанию, имеет два «унаследованных» метода с одинаковой сигнатурой, вызывая проблему ромба. Это смягчается либо требованием A
реализации самого метода, что устраняет неоднозначность, либо принуждением вызывающего объекта сначала привести объект A
к соответствующему интерфейсу, чтобы использовать реализацию этого метода по умолчанию (например, ((Ia) aInstance).Method();
).D
объект фактически будет содержать два отдельных A
объекта, и использование A
членов должно быть правильно квалифицировано. Если наследование от A
to B
и наследование от A
to C
помечены " virtual
" (например, " class B : virtual public A
"), C++ уделяет особое внимание созданию только одного A
объекта, и использование A
членов работает правильно. Если виртуальное и невиртуальное наследование смешаны, для каждого невиртуального наследования существует один виртуальный A
и невиртуальный путь наследования к . C++ требует явного указания, из какого родительского класса вызывается используемая функция, т.е. C++ не поддерживает явное повторное наследование, поскольку не было бы возможности определить, какой суперкласс использовать (т. е. класс появляется более одного раза в одном списке производных [класс Dog: public Animal, Animal]). C++ также позволяет создавать один экземпляр множественного класса с помощью механизма виртуального наследования (т. е . он будет ссылаться на один и тот же объект).A
A
Worker::Human.Age
Worker::Human
Musician::Human
D,B,C,A
, когда B в определении класса пишется перед C. Выбирается метод с наиболее конкретными классами аргументов (D>(B,C)>A) ; затем в том порядке, в котором родительские классы названы в определении подкласса (B>C). Однако программист может обойти это, задав определенный порядок разрешения методов или указав правило комбинирования методов. Это называется комбинацией методов, которую можно полностью контролировать. MOP ( протокол метаобъектов ) также предоставляет средства для изменения наследования, динамической диспетчеризации , создания экземпляров классов и других внутренних механизмов, не влияя на стабильность системы.D
встраивает две структуры B
, и C
обе имеют метод F()
, удовлетворяя таким образом интерфейс A
, компилятор будет жаловаться на «неоднозначный селектор» D.F()
, если он вызывается или если экземпляр D
присваивается переменной типа A
. B
и C
методы можно вызывать явно с помощью D.B.F()
или D.C.F()
.A,B,C
это интерфейсы, B,C
каждый из них может предоставить различную реализацию абстрактного метода , A
вызывая проблему ромба. Либо класс D
должен переопределить метод (тело которого может просто перенаправить вызов одной из суперреализаций), либо неоднозначность будет отклонена как ошибка компиляции. [8] До Java 8 Java не подвергалась риску проблемы Diamond, поскольку она не поддерживала множественное наследование, а методы интерфейса по умолчанию были недоступны.(individual as Person).printInfo();
. super<ChosenParentInterface>.someMethod()
B
и его предки будут проверяться перед классом C
и его предками, поэтому метод in A
будет унаследован через B
. Это общее с Io и Picolisp . В Perl это поведение можно переопределить с помощью того mro
или иного модуля для использования линеаризации C3 или других алгоритмов. [9]object
. Python создает список классов, используя алгоритм линеаризации C3 (или порядок разрешения методов (MRO)). Этот алгоритм накладывает два ограничения: дети предшествуют своим родителям, и если класс наследуется от нескольких классов, они сохраняются в порядке, указанном в кортеже базовых классов (однако в этом случае некоторые классы, расположенные выше в графе наследования, могут предшествовать классам, расположенным ниже в графе наследования). график [10] ). Таким образом, порядок разрешения метода: D
, B
, C
, A
. [11]D
, C
, A
, B
, A
], который уменьшается до [ D
, C
, B
, A
].Языки, допускающие только одиночное наследование , где класс может быть производным только от одного базового класса, не имеют проблемы ромба. Причина этого в том, что такие языки имеют не более одной реализации любого метода на любом уровне цепочки наследования независимо от повторения или размещения методов. Обычно эти языки позволяют классам реализовывать несколько протоколов , называемых интерфейсами в Java. Эти протоколы определяют методы, но не предоставляют конкретных реализаций. Эта стратегия использовалась ActionScript , C# , D , Java , Nemerle , Object Pascal , Objective-C , Smalltalk , Swift и PHP . [13] Все эти языки позволяют классам реализовывать несколько протоколов.
Более того, Ada , C#, Java, Object Pascal, Objective-C, Swift и PHP допускают множественное наследование интерфейсов (называемых протоколами в Objective-C и Swift). Интерфейсы подобны абстрактным базовым классам, которые определяют сигнатуры методов без реализации какого-либо поведения. («Чистые» интерфейсы, такие как интерфейсы Java до версии 7, не допускают никакой реализации или данных экземпляра в интерфейсе.) Тем не менее, даже когда несколько интерфейсов объявляют одну и ту же сигнатуру метода, как только этот метод реализован (определен) в любом месте цепочки наследования он переопределяет любую реализацию этого метода в цепочке над ним (в его суперклассах). Следовательно, на любом уровне цепочки наследования может существовать не более одной реализации любого метода. Таким образом, реализация метода с одним наследованием не демонстрирует проблему ромба даже при множественном наследовании интерфейсов. С введением реализации по умолчанию для интерфейсов в Java 8 и C# 8 все еще возможно создать проблему ромба, хотя это будет проявляться только как ошибка времени компиляции.