- Проблема алмаза
-
Проблема алмаза
В объектно-ориентированных языках программирования с поддержкой множественного наследования и структуры накопления знаний (knowledge organization) Проблема алмаза (diamond problem) — неопределенность, возникающая, когда два класса B и C наследуют от A, а класс D наследует от обоих классов B и C. Если метод класса D вызывает метод, определенный в классе A (и этот метод не был переопределен), а классы B и C по-своему переопределили этот метод, то тогда возникает вопрос — от какого класса его наследовать: B или C?
Например, в области разработки графических интерфейсов класс
Button(Кнопка) может наследовать от обоих классовRectangle(Прямоугольник) (для внешнего вида) иClickable(Доступен для кликанья мышкой) (для реализации функционала/обработки ввода), а классыRectangleиClickableнаследуют от классаObject(Объект). Теперь если вызвать методequals(Равно) для объектаButton, и в классеButtonне окажется такого метода, но вместо этого будет присутствовать переопределенный по-своему методequalsв обоих классахRectangleиClickable, то какой из методов должен быть вызван?Данная проблема получила свое название «Проблема алмаза» (diamond problem) благодаря очертаниям диаграммы наследования классов в этой ситуации, напоминающим очертания граненого алмаза. В данной статье, класс A обозначается в виде вершины, классы B и C по отдельности указываются ниже, а D соединяется с обоими в самом низу, образуя контур алмаза.
Решения
Различные языки программирования решают данную проблему следующими способами:
- C++ по умолчанию обрабатывает каждый путь наследования отдельно, в результате чего объект
Dбудет на самом деле содержать два разных объектаA, и использование членовAбудет соответствующим образом обработано. Если наследование отAкBи наследование отAкCоба помечаются как «virtual» (виртуальные) (например, «class B : virtual public A»), C++ специальным образом проследит за созданием только одного объектаA, и использование членовAбудет работать корректно. Если виртуальное и невиртуальное наследования смешиваются, то получается один виртуальныйAи один невиртуальныйAдля каждого пути невиртуального наследования кA. - Common Lisp пытается реализовать оба разумных поведения по умолчанию и возможность переопределять его. По умолчанию выбирается метод с наиболее специфичными аргументами для соответствующего класса; затем по порядку, в котором родительские классы указаны при определении подкласса. Однако, программист вполне может это переопределить путем указания специального порядка разрешения методов или указания правила для объединения методов.
- Eiffel обрабатывает подобную ситуацию путем выбора и переименования директив, в которых методы предка используются в потомках явно указанным образом. Это позволяет методам базового класса сообща использоваться в его потомках или даже предоставлять им отдельную копию базового класса.
- Perl и Io обрабатывают такую ситуацию путём указания порядка наследования классов в виде упорядоченного списка. В случае неопределенности, описанной выше, класс
Bи его предки будут проверены перед классомCи его предками, так что метод вAбудет унаследован отB. В Perl это поведение может быть переопределено при помощиmroили других модулей для применения C3-линеаризации, либо других алгоритмов. - Python должен разбирать ситуацию перед указанием классов нового стиля, все из которых имеют общего предка
object. Python создает список классов, которые будут искаться, начиная слева (D,B,A,C,A), а затем убирает все, кроме последнего подключения любого из повторяющихся классов. Таким образом, порядок разрешения выглядит следующим образом:D,B,C,A. - Scala использует метод разрешения имен при помощи поиска по шаблону, начиная справа, в ходе чего удаляется все, кроме последнего вхождения каждого модуля в итоговый список. Так, итоговый порядок будет выглядеть следующим образом: [
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.Литература
- 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.
