Введение
В программном обеспечении, наличие нескольких родительских классов.
Множественное наследование — это особенность некоторых объектно-ориентированных языков программирования, в которой объект или класс может наследовать признаки от более чем одного родительского объекта или родительского класса. Оно отличается от одиночного наследования, при котором объект или класс может наследовать только от одного конкретного объекта или класса. Множественное наследование на протяжении многих лет было предметом споров, и противники указывали на его повышенную сложность и неоднозначность в ситуациях, таких как «алмазная проблема», когда может быть неясно, от какого родительского класса унаследован конкретный признак, если более одного родительского класса реализует этот признак. Эту проблему можно решить различными способами, включая использование виртуального наследования. Также были предложены альтернативные методы композиции объектов, не основанные на наследовании, такие как миксины и трейты, для устранения этой неоднозначности.
Подробности
В объектно-ориентированном программировании (ООП) наследование описывает отношение между двумя классами, в котором один класс (дочерний класс) является подклассом родительского класса. Дочерний класс наследует методы и атрибуты родительского, что позволяет использовать общую функциональность. Например, можно создать класс "Млекопитающее" с характеристиками, такими как питание, размножение и т.д.; затем определить дочерний класс "Кот", который наследует эти характеристики, не требуя их явного программирования, при этом добавляя новые, например, охоту на мышей. Множественное наследование позволяет программистам одновременно использовать более одной полностью независимой иерархии, например, позволяя классу "Кот" наследовать от "Персонажа мультфильма", "Домашнего питомца" и "Млекопитающего" и получать доступ к функциям из всех этих классов.
Реализация
Языки, поддерживающие множественное наследование, включают: C++, Common Lisp (через Common Lisp Object System (CLOS)), EuLisp (через The 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) в более ранних версиях). Среда выполнения IBM System Object Model (SOM) поддерживает множественное наследование, и любой язык программирования, ориентированный на SOM, может реализовывать новые классы SOM, наследуемые от нескольких базовых классов. Некоторые объектно-ориентированные языки, такие как Swift, Java, Fortran (начиная с версии 2003 года), C# и Ruby реализуют одиночное наследование, хотя протоколы или интерфейсы предоставляют часть функциональности, присущей истинному множественному наследованию. PHP использует traits (черты) для наследования конкретных реализаций методов. Ruby использует модули для наследования нескольких методов.
Проблема с бриллиантами
"Диамантовая проблема" (иногда называемая "Смертельным алмазом смерти") — это неоднозначность, возникающая, когда два класса 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 соединяет их внизу, образуя форму алмаза.
Смягчение
У языков есть разные способы решения этих проблем повторного наследования. C# (начиная с C# 8.0) позволяет реализовывать методы интерфейса по умолчанию, что приводит к тому, что класс A, реализующий интерфейсы Ia и Ib с аналогичными методами, имеющими реализации по умолчанию, получает два "унаследованных" метода с одинаковой сигнатурой, вызывая проблему ромба. Она смягчается либо требованием к A реализовать метод самостоятельно, устраняя тем самым неоднозначность, либо принуждением вызывающего кода сначала привести объект A к соответствующему интерфейсу, чтобы использовать его реализацию по умолчанию для этого метода (например, ((Ia) aInstance).Method();). C++ по умолчанию рассматривает каждый путь наследования отдельно, поэтому объект D фактически содержит два отдельных объекта A, и обращение к членам A должно быть правильно квалифицировано. Если наследование от A к B и от A к C помечено как "виртуальное" (например, "class B : virtual public A"), C++ заботится о создании только одного объекта A, и обращение к членам A работает правильно. Если виртуальное и не виртуальное наследование смешаны, существует один виртуальный A и один не виртуальный A для каждого пути не виртуального наследования к A. C++ требует явно указывать, из какого родительского класса вызывается функция, например, Worker::Human.Age. C++ не поддерживает явное повторное наследование, поскольку не было бы способа определить, какой суперкласс использовать (например, наличие класса, появляющегося более одного раза в списке наследования [class Dog : public Animal, Animal]). C++ также позволяет создать один экземпляр класса, наследуемого несколько раз, с помощью механизма виртуального наследования (например, Worker::Human и Musician::Human будут ссылаться на один и тот же объект). Common Lisp CLOS пытается обеспечить как разумное поведение по умолчанию, так и возможность его переопределения. По умолчанию, простыми словами, методы сортируются в порядке D, B, C, A, если B указан перед C в определении класса. Выбирается метод с наиболее специфичными классами аргументов (D > (B, C) > A), затем в порядке, в котором родительские классы указаны в определении подкласса (B > C). Однако программист может переопределить это, указав конкретный порядок разрешения методов или правило для объединения методов. Это называется комбинацией методов, которая может быть полностью контролируемой. MOP (метаобъектный протокол) также предоставляет средства для изменения наследования, динамической диспетчеризации, инстанцирования класса и других внутренних механизмов без ущерба для стабильности системы. Curl разрешает наследовать повторно только классы, явно помеченные как общие. Общие классы должны определять вторичный конструктор для каждого обычного конструктора в классе. Обычный конструктор вызывается в первый раз, когда состояние для общего класса инициализируется через конструктор подкласса, а вторичный конструктор будет вызван для всех остальных подклассов. В Eiffel черты предков выбираются явно с помощью директив select и rename. Это позволяет совместно использовать черты базового класса между его потомками или предоставить каждому из них отдельную копию базового класса. Eiffel позволяет явно объединять или разделять черты, унаследованные от классов-предков. Eiffel автоматически объединяет черты, если они имеют одинаковое имя и реализацию. Автор класса имеет возможность переименовать унаследованные черты, чтобы разделить их. Многократное наследование часто встречается в разработке Eiffel; большинство эффективных классов в широко используемой библиотеке EiffelBase структур данных и алгоритмов, например, имеют двух или более родителей. Go предотвращает проблему ромба во время компиляции. Если структура D встраивает две структуры B и C, которые обе имеют метод F, тем самым удовлетворяя интерфейс A, компилятор будет жаловаться на "неоднозначный селектор", если вызывается D.F или если экземпляр D присваивается переменной типа A. Методы B и C можно вызывать явно с помощью D.B.F или D.C.F. Java 8 представляет методы по умолчанию в интерфейсах. Если A, B, C — интерфейсы, то B и C могут каждый предоставить различную реализацию абстрактного метода A, что вызывает проблему ромба. Либо класс D должен перереализовать метод (тело которого может просто перенаправить вызов в одну из суперреализаций), либо неоднозначность будет отклонена как ошибка компиляции. До Java 8 Java не подвергалась риску проблемы ромба, поскольку не поддерживала множественное наследование, а методы по умолчанию в интерфейсах были недоступны. JavaFX Script в версии 1.2 разрешает множественное наследование посредством использования миксинов. В случае конфликта компилятор запрещает прямое использование неоднозначной переменной или функции. К каждому унаследованному члену все еще можно получить доступ, приведя объект к интересующему миксину, например, (individual as Person).printInfo();. Kotlin разрешает множественное наследование интерфейсов, однако в сценарии проблемы ромба дочерний класс должен переопределить метод, вызывающий конфликт наследования, и указать, реализация какого родительского класса должна использоваться, например, super<ChosenParentInterface>.someMethod(). Logtalk поддерживает как интерфейсное, так и реализационное множественное наследование, позволяя объявлять псевдонимы методов, которые обеспечивают как переименование, так и доступ к методам, которые были бы замаскированы механизмом разрешения конфликтов по умолчанию. В OCaml родительские классы указываются по отдельности в теле определения класса. Методы (и атрибуты) наследуются в том же порядке, при этом каждый вновь унаследованный метод переопределяет любые существующие методы. OCaml выбирает последнее соответствующее определение в списке наследования классов, чтобы разрешить, какую реализацию метода использовать в случае неоднозначности. Чтобы переопределить поведение по умолчанию, достаточно квалифицировать вызов метода желаемым определением класса. Perl использует список классов для наследования в качестве упорядоченного списка. Компилятор использует первый найденный метод, выполняя поиск в глубину по списку суперклассов или используя C3-линеаризацию иерархии классов. Различные расширения предоставляют альтернативные схемы композиции классов. Порядок наследования влияет на семантику класса. В вышеупомянутой неоднозначности класс B и его предки будут проверены перед классом C и его предками, поэтому метод в A будет унаследован через B. Это справедливо для Io и Picolisp. В Perl это поведение можно переопределить, используя mro или другие модули для использования C3-линеаризации или других алгоритмов. Python имеет ту же структуру, что и Perl, но, в отличие от Perl, включает ее в синтаксис языка. Порядок наследования влияет на семантику класса. Python пришлось столкнуться с этим при введении новых классов стиля, все из которых имеют общего предка object. Python создает список классов, используя алгоритм C3-линеаризации (или порядок разрешения методов (MRO)). Этот алгоритм налагает два ограничения: дочерние элементы предшествуют своим родителям, и если класс наследуется от нескольких классов, они сохраняются в порядке, указанном в кортеже базовых классов (однако в этом случае некоторые классы, находящиеся выше в графе наследования, могут предшествовать классам, находящимся ниже в графе). Таким образом, порядок разрешения методов: D, B, C, A. Классы Ruby имеют ровно одного родителя, но также могут наследовать от нескольких модулей; определения классов Ruby выполняются, а (пере)определение метода затеняет любое ранее существовавшее определение во время выполнения. При отсутствии метапрограммирования во время выполнения это имеет примерно ту же семантику, что и разрешение в глубину справа налево. Scala разрешает многократную инстанциацию трейтов, что позволяет использовать множественное наследование, добавляя различие между иерархией классов и иерархией трейтов. Класс может наследовать только от одного класса, но может смешивать столько трейтов, сколько необходимо. Scala разрешает имена методов, используя поиск в глубину справа налево расширенных трейтов, прежде чем удалить все, кроме последнего вхождения каждого модуля в результирующем списке. Таким образом, порядок разрешения: [D, C, A, B, A], который сводится к [D, C, B, A]. Tcl разрешает несколько родительских классов; порядок указания в объявлении класса...
Logtalk supports both interface and implementation multi inheritance, allowing the declaration of method aliases that provide both renaming and access to methods that would be masked out by the default conflict resolution mechanism. In OCaml, parent classes are specified individually in the body of the class definition. Methods (and attributes) are inherited in the same order, with each newly inherited method overriding any existing methods. OCaml chooses the last matching definition of a class inheritance list to resolve which method implementation to use under ambiguities. To override the default behavior, one simply qualifies a method call with the desired class definition. Perl uses the list of classes to inherit from as an ordered list. The compiler uses the first method it finds by depth first searching of the superclass list or using the C3 linearization of the class hierarchy. Various extensions provide alternative class composition schemes. The order of inheritance affects the class semantics. In the above ambiguity, class B and its ancestors would be checked before class C and its ancestors, so the method in A would be inherited through B. This is shared with Io and Picolisp. In Perl, this behavior can be overridden using the mro or other modules to use C3 linearization or other algorithms. Python has the same structure as Perl, but, unlike Perl, includes it in the syntax of the language. The order of inheritance affects the class semantics. Python had to deal with this upon the introduction of new style classes, all of which have a common ancestor, object. Python creates a list of classes using the C3 linearization (or Method Resolution Order (MRO)) algorithm. That algorithm enforces two constraints: children precede their parents and if a class inherits from multiple classes, they are kept in the order specified in the tuple of base classes (however in this case, some classes high in the inheritance graph may precede classes lower in the graph). Thus, the method resolution order is: D, B, C, A.
Ruby classes have exactly one parent but may also inherit from multiple modules; ruby class definitions are executed, and the (re)definition of a method obscures any previously existing definition at the time of execution. In the absence of runtime metaprogramming this has approximately the same semantics as rightmost depth first resolution. Scala allows multiple instantiation of traits, which allows for multiple inheritance by adding a distinction between the class hierarchy and the trait hierarchy. A class can only inherit from a single class, but can mix in as many traits as desired. Scala resolves method names using a right first depth first search of extended 'traits', before eliminating all but the last occurrence of each module in the resulting list. So, the resolution order is: [D, C, A, B, A], which reduces down to [D, C, B, A]. Tcl allows multiple parent classes; the order of specification in the class declaration affects the name resolution for members using the C3 linearization algorithm. Languages that allow only single inheritance, where a class can only derive from one base class, do not have the diamond problem. The reason for this is that such languages have at most one implementation of any method at any level in the inheritance chain regardless of the repetition or placement of methods. Typically these languages allow classes to implement multiple protocols, called interfaces in Java. These protocols define methods but do not provide concrete implementations. This strategy has been used by ActionScript, C#, D, Java, Nemerle, Object Pascal, Objective C, Smalltalk, Swift and PHP. All these languages allow classes to implement multiple protocols. Moreover, Ada, C#, Java, Object Pascal, Objective C, Swift and PHP allow multiple inheritance of interfaces (called protocols in Objective C and Swift). Interfaces are like abstract base classes that specify method signatures without implementing any behaviour. ("Pure" interfaces such as the ones in Java up to version 7 do not permit any implementation or instance data in the interface.) Nevertheless, even when several interfaces declare the same method signature, as soon as that method is implemented (defined) anywhere in the inheritance chain, it overrides any implementation of that method in the chain above it (in its superclasses). Hence, at any given level in the inheritance chain, there can be at most one implementation of any method. Thus, single inheritance method implementation does not exhibit the Diamond Problem even with multiple inheritance of interfaces. With the introduction of default implementation for interfaces in Java 8 and C# 8, it is still possible to generate a Diamond Problem, although this will only appear as a compile time error.