- Ромбовидное наследование
-
Ромбовидное наследование (англ. diamond inheritance) — ситуация в объектно-ориентированных языках программирования с поддержкой множественного наследования, когда два класса
B
иC
наследуют отA
, а классD
наследует от обоих классовB
иC
. При этой схеме наследования может возникнуть неоднозначность: если метод классаD
вызывает метод, определенный в классеA
(и этот метод не был переопределен), а классыB
иC
по-своему переопределили этот метод, то от какого класса его наследовать:B
илиC
?Например, в области разработки графических интерфейсов класс
Button
(«Кнопка») может одновременно наследовать от классаRectangle
(«Прямоугольник», для внешнего вида) и от классаClickable
(«Доступен для кликанья мышкой», для реализации функционала/обработки ввода), аRectangle
иClickable
наследуют от классаObject
(«Объект»). Если вызвать методequals
(«Равно») для объектаButton
, и в классеButton
не окажется такого метода, но в классеObject
будет присутствовать методequals
по-своему переопределенный как в классеRectangle
, так и вClickable
, то какой из методов должен быть вызван?Проблема ромба (англ. diamond problem) получила свое название благодаря очертаниям диаграммы наследования классов в этой ситуации. В данной статье, класс
A
обозначается в виде вершины, классыB
иC
по отдельности указываются ниже, аD
соединяется с обоими в самом низу, образуя ромб.Содержание
Решения
Различные языки программирования решают проблему ромбовидного наследования следующими способами:
- C++ по умолчанию не создает ромбовидного наследования: компилятор обрабатывает каждый путь наследования отдельно, в результате чего объект
D
будет на самом деле содержать два разных подобъектаA
, и при использовании членовA
потребуется указать путь наследования (B::A
илиC::A
). Чтобы сгенерировать ромбовидную структуру наследования, необходимо воспользоваться виртуальным наследованием классаA
на нескольких путях наследования: если оба наследования отA
кB
и отA
кC
помечаются спецификаторомvirtual
(например,class B : virtual public A
), C++ специальным образом проследит за созданием только одного подобъектаA
, и использование членовA
будет работать корректно. Если виртуальное и невиртуальное наследования смешиваются, то получается один виртуальный подобъектA
и по одному невиртуальному подобъектуA
для каждого пути невиртуального наследования кA
. При виртуальном вызове метода виртуального базового класса используется так называемое правило доминирования: компилятор запрещает виртуальный вызов метода, который был перегружен на нескольких путях наследования. - Common Lisp пытается реализовать и разумное поведение по умолчанию, и возможность изменить его. По умолчанию выбирается метод с наиболее специфичными классами аргументов; затем, методы выбираются по порядку, в котором родительские классы указаны при определении подкласса. Однако программист вполне может изменить это поведение путем указания специального порядка разрешения методов или указания правила для объединения методов.
- Eiffel обрабатывает подобную ситуацию при помощи директив
select
иrename
, и методы предка, которые используются в потомках, указываются явно. Это позволяет совместно использовать методы родительского класса в потомках или предоставлять им отдельную копию родительского класса. - Perl и Io обрабатывают наследования через поиск в глубину в том порядке, который используется в определении класса. Класс
B
и его предки будут проверены перед классомC
и его предками, так что метод вA
будет унаследован отB
; список разрешения — [D
,B
,A
,C
]. При этом в Perl данное поведение может быть изменено при помощиmro
или других модулей для применения C3-линеаризации (как в Python) или других алгоритмов. - В Python проблема ромба остро встала в версии 2.3 после введения классов с общим предком
object
; начиная с этой версии было решено создавать список разрешения при помощи C3-линеаризации[1]. В случае ромба, это означит поиск в глубину начиная слева (D
,B
,A
,C
,A
), а затем удаление из списка всех, кроме последнего включения каждого класса, который в списке повторяется. Следовательно, итоговый порядок разрешения выглядит так: [D
,B
,C
,A
]. - Scala список разрешения создается аналогично Python, но через поиск в глубину начиная справа. Следовательно, предварительный список разрешения ромба — [
D
,C
,A
,B
,A
], в после удаления повторений — [D
,C
,B
,A
]. - JavaFX Script, начиная с версии 1.2, позволяет множественное наследование за счет применения примесей. В случае конфликта, компилятор запрещает прямое использование неопределенных переменных или функции. К каждому наследуемому члену по-прежнему будет возможен доступ за счет приведения объекта к нужной примеси, например,
(individual as Person).printInfo();
.
Прочие примеры
Языки, допускающие лишь простое наследование (как например, Ада, Objective-C, PHP, C#, Delphi/Free Pascal и Java), предусматривают множественное наследование интерфейсов (в Objective-C называемые протоколами). Интерфейсы по сути являются абстрактными базовыми классами, все методы которых также абстрактны, и где отсутствуют поля. Таким образом, проблема не возникает, так как всегда будет только одна реализация определенного метода или свойства, не допуская возникновения неопределенности.
Проблема ромба не ограничивается лишь наследованием. Она также возникает в таких языках, как Си и C++, когда заголовочные файлы A, B, C и D, а также отдельные предкомпилированные заголовки, созданные из B и C, подключаются (при помощи инструкции
#include
) один к другому по ромбовидной схеме, указанной вверху. Если эти два предкомпилированных заголовка объединяются, объявления в A дублируются, и директива защиты подключения#ifndef
становится неэффективной. Также проблема обнаруживается при объединении стеков подпрограммного обеспечения; например, если A — это база данных, а B и C — кэши, то D может запросить как B, так и C подтвердить (COMMIT) выполнение транзакции, приводя к дублирующим вызовам подтверждений A.Примечания
- ↑ The Python 2.3 Method Resolution Order (англ.). Архивировано из первоисточника 12 апреля 2012. Проверено 15 мая 2010.
Литература
- Eddy Truyen; Wouter Joosen, Bo Jørgensen, Petrus Verbaeten (2004). «A Generalization and Solution to the Common Ancestor Dilemma Problem in Delegation-Based Object Systems». Proceedings of the 2004 Dynamic Aspects Workshop (103-119).
Категория:- Наследование (программирование)
- C++ по умолчанию не создает ромбовидного наследования: компилятор обрабатывает каждый путь наследования отдельно, в результате чего объект
Wikimedia Foundation. 2010.