Заметьте, как тщательно был спроектирован механизм, дающий разработчикам шанс забыть об устаревшем стиле разбора вариантов (case-by-case). Если вы действительно хотите перехитрить динамическое связывание и отдельно проверять каждый вариант типа, вы можете это сделать, хотя вам и придется немало потрудиться. Так, вместо обычного f.display, использующего ОО-механизмы полиморфизма и динамического связывания, можно, - но не рекомендуется, - писать:
display (f: FIGURE) is
-- Отобразить f, используя алгоритм,
-- адаптируемый к истинной природе объекта.
local
r: RECTANGLE; t: TRIANGLE; p: POLYGON; s: SQUARE
sg: SEGMENT; e: ELLIPSE; c: CIRCLE;?
do
r ?= f; if r /= Void then "Использовать алгоритм вывода прямоугольника" end
t ?= f; if t /= Void then "Использовать алгоритм вывода треугольника" end
c ?= f; if c /= Void then "Использовать алгоритм вывода окружности" end
... и т.д. ...
end
На практике такая схема даже хуже, чем кажется, так как структура наследования имеет несколько уровней, а значит, усложнения управляющих конструкций не избежать.
Из-за трудностей написания таких закрученных конструкций попытки присваивания новичкам вряд ли придет в голову использовать их вместо привычной ОО-схемы. Однако и опытные специалисты должны помнить о возможности неправильного использования конструкции.
Немного похожий на попытку присваивания механизм "сужения" (narrowing) есть в языке Java. В случае несоответствия типов он выдает исключение. Это похоже на самоубийство, неуспех присваивания вовсе не является чем-то ненормальным, это ожидаемый результат. Оператор instanceof в языке Java выполняет проверку типов на совместимость.
Из-за отсутствия в языке универсальности Java активно использует оба механизма. Отчасти это связано с тем, что в отсутствие множественного наследования Java не содержит класса NONE, а потому не может выделить эквиваленту Void надежное место в собственной системе типов.
Типизация и повторное объявление
Повторное объявление компонентов не требует сохранения сигнатуры. Пока оно виделось нам как замена одного алгоритма другим или - для отложенного компонента - запись алгоритма, соответствующего ранее заданной спецификации.
Но, воплощая идею о том, что класс способен предложить более специализированную версию элемента, описанного его предком, мы вынуждены иногда изменять типы данных. Приведем два характерных примера.
Вот простой пример переопределения типа. Рассмотрим понятие устройства, включив предположение о том, что для любого устройства есть альтернатива, так что устройство можно заменить, если оно по каким-либо причинам недоступно:
class DEVICE feature
alternate: DEVICE
set_alternate (a: DEVICE) is
-- Пусть a - альтернативное устройство.
do
alternate := a
end
... Прочие компоненты ...
end
Принтер является устройством, так что использование наследования оправдано. Но альтернативой принтера может быть только принтер, но не дисковод для компакт-дисков или сетевая карта, - поэтому мы должны переопределить тип:
Рис. 16.6. Устройства и принтеры
class PRINTER inherit
DEVICE
redefine alternate, set_alternate
feature
alternate: PRINTER
set_alternate (a: PRINTER) is
-- Пусть a - альтернативное устройство.
... Тело как у класса DEVICE ...
... Прочие компоненты ...
end
В этом и проявляется специализирующая природа наследования.
Одно- и двусвязные элементы
В следующем примере мы обратимся к базовым структурам данных. Рассмотрим библиотечный класс LINKABLE, описывающий односвязные элементы, используемые в LINKED_LIST - одной из реализаций списков. Вот частичное описание класса:
indexing
description: "Односвязные элементы списка"
class LINKABLE [G] feature
item: G
right: LINKABLE [G]
put_right (other: LINKABLE [G]) is
-- Поместить other справа от текущего элемента.
do right := other end
... Прочие компоненты ...
end
Рис. 16.7. Односвязный элемент списка
Ряд приложений требуют двунаправленных списков. Класс TWO_WAY_LIST - наследник LINKED_LIST должен быть также наследником класса BI_LINKABLE, являющегося наследником класса LINKABLE.
Рис. 16.8. Параллельные иерархии
Двусвязный элемент списка имеет еще одно поле:
Рис. 16.9. Двусвязный элемент списка
В состав двунаправленных списков должны входить лишь двусвязные элементы (хотя последние, в силу полиморфизма, вполне можно внедрять и в однонаправленные структуры). Переопределив right и put_right, мы гарантируем однородность двусвязных списков.
indexing
description: "Элементы двусвязного списка"
class BI_LINKABLE [G] inherit
LINKABLE [G]
redefine right, put_right end
feature
left, right: BI_LINKABLE [G]
put_right (other: BI_LINKABLE [G]) is
-- Поместить other справа от текущего элемента.
do
right := other
if other /= Void then other.put_left (Current) end
end
put_left (other: BI_LINKABLE [G]) is
-- Поместить other слева от текущего элемента.
... Упражнение для читателя ...
... Прочие компоненты ...
invariant
right = Void or else right.left = Current
left = Void or else left.right = Current
end
(Попробуйте написать put_left. Здесь скрыта ловушка! См. приложение A.)
Правило повторного объявления типов
Примеры, рассмотренные выше, несмотря на все их различия, объединяет необходимость повторного объявления типов. Спуск по иерархии наследования означает специализацию классов, и в соответствии со специализацией изменяются типы функций и типы аргументов подпрограмм, как, например, a в set_alternate и other в put_right; изменяются типы запросов - alternate и right.
Этот аспект повторного объявления выражает следующее правило:
Правило повторного объявления типов
При повторном объявлении компонента можно заменить тип компонента (для атрибутов и функций) или тип формального параметра (для подпрограмм) любым совместимым типом.
Правило использует понятие совместимости типов. Связка "или", стоящая в тексте правила, не исключает того, что при повторном объявлении функции мы можем одновременно изменить как тип результата функции, так и тип одного или нескольких ее аргументов.
Любое повторное объявление ведет к специализации, а, следовательно, к изменению типов. Так, с переходом к двунаправленным спискам параметры и результаты функций сменили свой тип на BI_LINKABLE. Отсюда становится понятен тот термин, которым часто описывают политику редекларации типов, - ковариантная типизация (covariant typing), где приставка "ко" указывает на параллельное изменение типов при спуске по диаграмме наследования.
Ковариантная типизация таит в себе немало проблем, которые возникают у создателей компиляторов, нередко перекладывающих их решение на плечи разработчиков приложений.
Правило повторного объявления типов способно свести на нет целый ряд преимуществ наследования. Почему это происходит и каково решение данной проблемы?
Рассмотрим пример с участием класса LINKED_LIST. Пусть мы имеем процедуру добавления в список нового элемента с заданным значением, который вставляется справа от текущего элемента. В деталях процедуры нет ничего необычного, но все же обратим внимание на потребность создания локальной сущности new типа LINKABLE, представляющей элемент списка, который будет создан и включен в список.
Рис. 16.10. Добавление элемента
put_right (v: G) is
-- Вставить элемент v справа от курсора.
-- Не передвигать курсор.