Множественное наследование — это особенность некоторых объектно-ориентированных языков программирования , в которых объект или класс может наследовать свойства более чем от одного родительского объекта или родительского класса . Оно отличается от одиночного наследования, когда объект или класс может наследовать только от одного конкретного объекта или класса.
Множественное наследование было спорным вопросом в течение многих лет, [1] [2] с оппонентами, указывающими на его возросшую сложность и неоднозначность в таких ситуациях, как «проблема алмаза», где может быть неоднозначно, от какого родительского класса унаследована конкретная функция, если более одного родительского класса реализуют эту функцию. Это можно решить разными способами, включая использование виртуального наследования . [3] Для решения неоднозначности также были предложены альтернативные методы композиции объектов, не основанные на наследовании, такие как миксины и черты .
В объектно-ориентированном программировании (ООП) наследование описывает связь между двумя классами, в которой один класс ( дочерний класс) подклассифицирует родительский класс . Дочерний класс наследует методы и атрибуты родителя, что позволяет использовать общую функциональность. Например, можно создать переменный класс Mammal с такими функциями, как прием пищи, размножение и т. д.; затем определить дочерний класс Cat , который наследует эти функции без необходимости явно их программировать, добавляя при этом новые функции, такие как преследование мышей .
Множественное наследование позволяет программистам использовать более одной полностью ортогональной иерархии одновременно, например, позволяя классу Cat наследовать от Cartoon Character , Pet и Mammal и получать доступ к функциям из всех этих классов.
Языки, которые поддерживают множественное наследование, включают: C++ , Common Lisp (через Common Lisp Object System (CLOS)), EuLisp (через EuLisp Object System 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?
Например, в контексте разработки программного обеспечения GUI класс может наследовать от обоих классов (для внешнего вида) и (для функциональности/обработки ввода), а классы и оба наследуют от класса. Теперь, если метод вызывается для объекта и такого метода нет в классе, но есть переопределенный метод в или (или в обоих), какой метод должен быть в конечном итоге вызван?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
и один невиртуальный A
для каждого невиртуального пути наследования to A
. C++ требует явного указания того, из какого родительского класса вызывается используемая функция, то есть Worker::Human.Age
. C++ не поддерживает явное повторное наследование, так как не было бы способа квалифицировать, какой суперкласс использовать (то есть наличие класса, появляющегося более одного раза в одном списке вывода [class Dog : public Animal, Animal]). C++ также позволяет создать один экземпляр множественного класса с помощью механизма виртуального наследования (то есть 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 не была подвержена риску проблемы ромба, поскольку не поддерживала множественное наследование, а методы интерфейса по умолчанию были недоступны.(individual as Person).printInfo();
. super<ChosenParentInterface>.someMethod()
B
и его предки будут проверяться до класса C
и его предков, поэтому метод в 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, не допускают никакой реализации или данных экземпляра в интерфейсе.) Тем не менее, даже когда несколько интерфейсов объявляют одну и ту же сигнатуру метода, как только этот метод реализуется (определяется) где-либо в цепочке наследования, он переопределяет любую реализацию этого метода в цепочке выше (в его суперклассах). Следовательно, на любом заданном уровне в цепочке наследования может быть не более одной реализации любого метода. Таким образом, реализация метода с одиночным наследованием не демонстрирует проблему Diamond даже при множественном наследовании интерфейсов. С введением реализации по умолчанию для интерфейсов в Java 8 и C# 8 все еще возможно сгенерировать проблему Diamond, хотя это будет выглядеть только как ошибка времени компиляции.