Основы объектно-ориентированного программирования
Шрифт:
Запрет на дублирование имен касается лишь эффективных компонентов. Если один или более компонентов с омонимичными именами являются отложенными, их можно фактически слить воедино, поскольку отсутствует несовместимость реализаций. Подробнее мы поговорим об этом чуть ниже.
Приведенные правила просты и интуитивны. Чтобы в последний раз нам убедиться в их правильном понимании, построим простой пример, демонстрирующий допустимые и недопустимые варианты наследования.
Рис. 15.20. Два варианта наследования
Класс D наследует this_one_OK дважды, один раз от B, другой раз - от C. Конфликта имен не возникает, поскольку данный компонент будет использоваться совместно. На самом деле, это - один компонент предка A.
Два компонента portend_trouble ("предвещающие беду") заслуженно получили такое имя. Они различны, потому их появление в D ведет к конфликту имен, делая класс некорректным. (У них разные типы, но и одинаковые типы никак не повлияли бы на ход нашего обсуждения.)
Переименовав один из компонентов, мы с легкостью сделаем D корректным:
Конфликт переопределений
Пока в ходе наследования мы меняли лишь имена. А что, если промежуточный предок, такой, как B или C (см. последний рисунок), переопределит дублируемо наследуемый компонент? При динамическом связывании это может привести к неоднозначности в D.
Проблему решают два простых механизма: отмена определения (undefinition) и выделение (selection). Как обычно, вы сами примете участие в их разработке и убедитесь в том, что при четкой постановке задачи нужная конструкция языка становится совершенно очевидной.
Пусть дублируемо наследуемый компонент переопределяется в одной из ветвей:
Рис. 15.21. Переопределение - причина потенциальной неоднозначности
Класс B переопределяет f. Поэтому в D этот компонент представлен в двух вариантах: результат переопределения в B и исходный вариант из A, полученный через класс C. (Можно предполагать, что и C переопределяет f, но это не внесет в наше рассуждение ничего нового.) Такое положение дел отличается от предыдущих случаев, в которых мы имели лишь один вариант компонента, возможно, наследуемый под разными именами.
Что произойдет в результате? Ответ зависит от того, под одним или разными именами класс D наследует варианты компонентов. Подразумевает ли дублируемое наследование репликацию или совместное использование? Рассмотрим эти случаи по порядку.
Конфликт при совместном использовании: отмена определения и соединение компонентов
Предположим вначале, что две версии наследуются под одним и тем же именем. Это случай совместного использования. Одному имени должен в точности соответствовать один компонент. Возможны три ситуации.
1 Если одна версия отложена, а другая - эффективна, то сложностей не возникает, будет использован эффективный вариант компонента. Заметим, что этот случай явно предусмотрен правилом одного имени: речь в нем идет лишь о конфликте имен двух эффективных версий.
2 Каждая версия эффективна, однако обе они переопределяются в D в предложении redefine. Проблемы снова не возникает, поскольку обе версии сливаются в одну, переопределяемую в тексте класса.
3 Обе версии эффективны, но обе не переопределяются, тогда действительно возникает конфликт имен. Класс D будет отвергнут, как нарушающий правило одного имени.
Нередко (3) означает ошибку: создана неоднозначность имен, и ее необходимо исправить. Тривиальным решением проблемы является переименование одного из вариантов, но тогда мы от рассматриваемого случая совместного использования переходим к репликации, изучаемой ниже.
Есть и другая, более изощренная возможность решения конфликта (3). Она состоит в том, чтобы позволить одному из вариантов "взять верх" над другим. Дальнейшее очевидно - свести эту ситуацию к (1), сделав один из двух вариантов отложенным.
Правила переопределения дают возможность переопределить компонент f как отложенный, хотя для этого и потребуется ввести промежуточный класс, скажем C', - наследника C, единственная роль которого - в переопределении отложенного f . Затем класс D должен быть порожден не от C, а от C'. Сложно и некрасиво. Вместо этого нам нужен простой языковой механизм: undefine. В секции наследования класса он приводит к появлению нового предложения:
Синтаксически предложение undefine следует за rename (всякая отмена определения должна действовать на окончательный вариант имени компонента), но до redefine (прежде, чем что-то переопределять, мы должны позаботиться об отмене ненужных определений).
Признаком того, что предлагаемый языковой механизм желателен, почти всегда является его направленность на решение нескольких проблем (соответственно, плохой механизм создает больше проблем, чем решает). Механизм отмены определений отвечает этому требованию: он позволяет соединять компоненты в условиях множественного (не обязательно - дублируемого) наследования. Пусть мы хотим свести воедино две абстракции:
Рис. 15.22. Два родителя и слияние компонентов