Ромбовидное наследование

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Диаграмма наследования классов в виде ромба.

Ромбовидное наследование (англ. diamond inheritance) — ситуация в объектно-ориентированных языках программирования с поддержкой множественного наследования, когда два класса B и C наследуют от A, а класс D наследует от обоих классов B и C. При этой схеме наследования может возникнуть неоднозначность: если метод класса D вызывает метод, определенный в классе A (и этот метод не был переопределен в классе D), а классы 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.

Примечания[править | править код]

  1. The Python 2.3 Method Resolution Order (англ.). Проверено 15 мая 2010. Архивировано 12 апреля 2012 года.

Литература[править | править код]

  • 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). Используется устаревший параметр |coauthors= (справка)