Множественное наследование
Пример повышенной сложности
Вот более сложный пример применения разных аспектов дублируемого наследования.
Проблема, близкая по духу нашему примеру, возникла из интересного обсуждения в основной книге по C++ [Stroustrup 1991].
Рассмотрим класс WINDOW с процедурой display и двумя наследниками: WINDOW_WITH_BORDER и WINDOW_WITH_MENU. Эти классы описывают абстрактные окна, первое из них имеет рамку, а второе поддерживает меню. Переопределяя display, каждый класс выводит на экран стандартное окно, а затем добавляет к нему рамку (в первом случае) и меню (во втором).
Опишем окно с рамкой и с поддержкой меню. В результате мы породим класс WINDOW_WITH_BORDER_AND_MENU.
Переопределим метод display в новом классе; новая версия вначале вызывает исходную, затем строит рамку, а потом строит меню. Исходный класс WINDOW имеет вид:
class WINDOW feature display is -- Отобразить окно (общий алгоритм) do ... end ... Другие компоненты ... end
Наследник WINDOW_WITH_BORDER осуществляет вызов родительской версии display и затем отображает рамку. В дублируемом наследовании нет необходимости, достаточно воспользоваться механизмом Precursor:
class WINDOW_WITH_BORDER inherit WINDOW redefine display end feature -- Output display is -- Рисует окно и его рамку. do Precursor draw_border end feature {NONE} -- Implementation draw_border is do ... end ... end
Обратите внимание на процедуру draw_border, рисующую рамку окна. Она скрыта от клиентов класса WINDOW_WITH_BORDER (экспорт классу NONE ), поскольку для них вызов draw_border не имеет смысла. Класс WINDOW_WITH_MENU аналогичен:
class WINDOW_WITH_MENU inherit WINDOW redefine display end feature -- Output display is -- Рисует окно и его меню. do Precursor draw_menu end feature {NONE} -- Implementation draw_menu is do ... end ... end
Осталось описать общего наследника WINDOW_WITH_BORDER_AND_MENU этих двух классов, дублируемого потомка WINDOW. Предпримем первую попытку:
indexing WARNING: "Первая попытка - версия не будет работать корректно!" class WINDOW_WITH_BORDER_AND_MENU inherit WINDOW_WITH_BORDER redefine display end WINDOW_WITH_MENU redefine display end feature display is -- Рисует окно,его рамку и меню. do Precursor {WINDOW_WITH_BORDER} Precursor {WINDOW_WITH_MENU} end ... end
Заметьте: при каждом обращении к Precursor мы вынуждены называть имя предка. Каждый предок имеет собственный компонент display, переопределенный под тем же именем.
Впрочем, как замечает Страуструп, это решение некорректно: версии родителей дважды вызывают исходную версию display класса WINDOW, что приведет к появлению "мусора" на экране. Для исправления ситуации добавим еще один класс, получив тройку наследников класса WINDOW:
indexing note: "Это корректная версия" class WINDOW_WITH_BORDER_AND_MENU inherit WINDOW_WITH_BORDER redefine display export {NONE} draw_border end WINDOW_WITH_MENU redefine display export {NONE} draw_menu end WINDOW redefine display end feature display is -- Рисует окно,его рамку и меню. do Precursor {WINDOW} draw_border draw_menu end ... end
Заметьте, что компоненты draw_border и draw_menu в новом классе являются скрытыми, поскольку мы не видим причин, по которым клиенты WINDOW_WITH_BORDER_AND_MENU могли бы их вызывать непосредственно.
Несмотря на активное применение дублируемого наследования, класс переопределяет все унаследованные им варианты display, что делает выражения select ненужными. В этом состоит преимущество спецификатора Precursor в сравнении с репликацией компонентов.
Неплохим тестом на понимание дублируемого наследования станет решение этой задачи без применения Precursor, путем репликации компонентов промежуточных классов. При этом, разумеется, вам понадобится select (см. упражнение 15.10).
В полученном варианте класса присутствует лишь совместное использование, но не репликация компонентов. Расширим пример Страуструпа: пусть WINDOW имеет запрос id (возможно, целого типа), направленный на идентификацию окон. Если идентифицировать любое окно только одним "номером", то id будет использоваться совместно, и нам не придется ничего менять. Если же мы хотим проследить историю окна, то экземпляр WINDOW_WITH_BORDER_AND_MENU будет иметь три id - независимых "номера". Новый текст класса комбинирует совместное использование и репликацию id (изменения в тексте класса помечены стрелками):
indexing note: "Усложненная версия с независимыми id." class WINDOW_WITH_BORDER_AND_MENU inherit WINDOW_WITH_BORDER rename id as border_id redefine display export {NONE} draw_border end WINDOW_WITH_MENU rename id as menu_id redefine display export {NONE} draw_menu end WINDOW rename id as window_id redefine display select window_id end feature .... Остальное, как ранее... end
Обратите внимание на необходимость выбора ( select ) одного из вариантов id.
Дублируемое наследование и универсальность
В завершение мы должны рассмотреть особый случай дублируемого наследования. Он касается компонентов, содержащих родовые параметры. Рассмотрим следующую схему (подобная ситуация может возникнуть не только при прямом, но и при косвенном дублируемом наследовании):
class A [G] feature f: G;... end class B inherit A [INTEGER] A [REAL] end
В классе B по правилу дублируемого наследования компонент f должен использоваться совместно. Но из-за универсализации возникает неоднозначность, - какой результат должен возвращать компонент - real или integer? Та же проблема возникнет, если f имеет параметр типа G.
Подобная неоднозначность недопустима. Отсюда правило:
Универсальность в правиле дублируемого наследования
Тип компонента, совместно используемого в правиле дублируемого наследования, а также тип любого из его аргументов не может быть родовым параметром класса, от которого произошло дублируемое наследование компонента.
Для устранения неоднозначности можно выполнить переименование в точке наследования.
Правила об именах
(В этом разделе мы только формализуем сказанное выше, поэтому при первом чтении книги его можно пропустить.)
Мы уже видели, что в случае возможной неоднозначности конфликты имен пресекаются, хотя некоторые ситуации бывают вполне корректны. Чтобы в представлении множественного и дублируемого наследования не оставить никакой неоднозначности, полезно обобщить ограничения на конфликт имен в едином правиле: Заканчивая этот раздел, сведем изложенный ранее материал в единое правило:
Конфликты имен: определение и правило
В классе, образованном в результате множественного наследования, возникает конфликт имен, если два компонента, наследованные от разных родителей, имеют одно и то же финальное имя.
Конфликт имен делает класс некорректным за исключением следующих случаев:
- Оба компонента унаследованы от общего предка, и ни один из них не получен повторным объявлением версии предка.
- Оба компонента имеют совместимые сигнатуры, и, по крайней мере, один из них наследуется в отложенной форме.
- Оба компонента имеют совместимые сигнатуры и переопределяются в новом классе.
Ситуация (1) описывает совместное использование при дублируемом наследовании.
Для случая (2) "наследование в отложенной форме" возможно по двум причинам: либо отложенная форма задана родительским классом, либо компонент был эффективным, но порожденный класс отменил его реализацию ( undefine ).
Ситуации (2) и (3) рассматриваются отдельно, однако, их можно представить как один вариант - вариант соединения (join). Переходя к n компонентам ( n >= 2 ), можно сказать, что ситуации (2) и (3) возникают, когда от разных родителей класс принимает n одноименных компонентов с совместимыми сигнатурами. Конфликт имен не делает класс некорректным, если эти компоненты могут быть соединены, иными словами:
- все n компонентов отложены, так что некому вызвать конфликт определений;
- существует единственный эффективный компонент. Его реализация станет реализацией остальных компонентов;
- два или несколько компонентов эффективны. Класс должен их переопределить. Новая реализация будет использоваться как для переопределяемых компонентов, так и для любых отложенных компонентов, участвующих в конфликте.
И, наконец, точное правило употребления конструкции Precursor. Если в переопределении используется Precursor, то неоднозначность может возникнуть из-за того, что неясно, версию какого родителя следует вызывать. Чтобы решить эту проблему, следует использовать вызов вида Precursor {PARENT} (...), где PARENT - имя желаемого родителя. В остальных случаях указывать имя родителя не обязательно.