Основы объектно-ориентированного программирования - [262]
Система называется системно-корректной (system-valid), если при ее выполнении не происходит нарушения типов.
В идеале оба понятия должны совпадать. Однако мы уже видели, что классово-корректная система в условиях наследования, ковариантности и скрытия потомком может не быть системно-корректной. Назовем такую ошибку нарушением системной корректности (system validity error).
Практический аспект
Простота проблемы создает своеобразный парадокс: пытливый новичок построит контрпример за считанные минуты, в реальной практике изо дня в день возникают ошибки классовой корректности систем, но нарушения системной корректности даже в больших, многолетних проектах возникают исключительно редко.
Однако это не позволяет игнорировать их, а потому мы приступаем к изучению трех возможных путей решения данной проблемы.
Далее мы будем затрагивать весьма тонкие и не столь часто дающие о себе знать аспекты объектного подхода. Читая книгу впервые, вы можете пропустить оставшиеся разделы этой лекции. Если вы лишь недавно занялись вопросами ОО-технологии, то лучше усвоите этот материал после изучения лекций 1-11 курса "Основы объектно-ориентированного проектирования", посвященной методологии наследования, и в особенности лекции 6 курса "Основы объектно-ориентированного проектирования", посвященной методологии наследования.
Корректность систем: первое приближение
Давайте сконцентрируемся вначале на проблеме ковариантности, более важной из двух рассматриваемых. Этой теме посвящена обширная литература, предлагающая ряд разнообразных решений.
Контравариантность и безвариантность
Контравариантность устраняет теоретические проблемы, связанные с нарушением системной корректности. Однако при этом теряется реалистичность системы типов, по этой причине рассматривать этот подход в дальнейшем нет никакой необходимости.
Оригинальность языка C++ в том, что он использует стратегию безвариантности (novariance), не позволяя менять тип аргументов в переопределяемых подпрограммах! Если бы язык C++ был строго типизированным языком, его системной типов было бы трудно пользоваться. Простейшее решение проблемы в этом языке, как и обход иных ограничений C++ (скажем, отсутствия ограниченной универсальности), состоит в использовании кастинга - приведения типа, что позволяет полностью игнорировать имеющийся механизм типизации. Это решение не кажется привлекательным. Заметим, однако, что ряд предложений, обсуждаемых ниже, будет опираться на безвариантность, смысл которой придаст введение новых механизмов работы с типами взамен ковариантного переопределения.
Использование родовых параметров
Универсальность лежит в основе интересной идеи, впервые высказанной Францем Вебером (Franz Weber). Объявим класс SKIER1, ограничив универсализацию родового параметра классом ROOM:
>class SKIER1 [G -> ROOM] feature
>accommodation: G
>accommodate (r: G) is ... require ... do accommodation := r end
>end
Тогда класс GIRL1 будет наследником SKIER1 [GIRL_ROOM] и т. д. Тем же приемом, каким бы странным он не казался на первый взгляд, можно воспользоваться и при отсутствии параллельной иерархии: class SKIER [G -> SKIER].
Этот подход позволяет решить проблему ковариантности. При любом использовании класса необходимо задать фактический родовой параметр ROOM или GIRL_ROOM, так что неверная комбинация просто становится невозможной. Язык становится безвариантным, а система полностью отвечает потребностям ковариантности благодаря родовым параметрам.
К сожалению, эта техника неприемлема как общее решение, поскольку ведет к разрастанию списка родовых параметров, по одному на каждый тип возможного ковариантного аргумента. Хуже того, добавление ковариантной подпрограммы с аргументом, тип которого отсутствует в списке, потребует добавления родового параметра класса, а, следовательно, изменит интерфейс класса, повлечет изменения у всех клиентов класса, что недопустимо.
Типовые переменные
Ряд авторов, среди которых Ким Брюс (Kim Bruce), Дэвид Шенг (David Shang) и Тони Саймонс (Tony Simons), предложили решение на основе типовых переменных (type variables), значениями которых являются типы. Их идея проста:
[x]. взамен ковариантных переопределений разрешить объявление типов, использующее типовые переменные;
[x]. расширить правила совместимости типов для управления такими переменными;
[x]. считать язык (в остальном) безвариантным;
[x]. обеспечить возможность присваивания типовым переменным в качестве значений типы языка.
Подробное изложение этих идей читатели могут найти в ряде статей по данной тематике, а также в публикациях Карделли (Cardelli), Кастаньи (Castagna), Вебера (Weber) и др. Начать изучение вопроса можно с источников, указанных в библиографических заметках к этой лекции. Мы же не будем заниматься этой проблемой, и вот почему.
[x]. Надлежаще реализованный механизм типовых переменных относится к категории, позволяющей использовать тип без полной его спецификации. Эта же категория включает универсальность и закрепление объявлений. Этот механизм мог бы заменить другие механизмы этой категории. Вначале это можно истолковать в пользу типовых переменных, но результат может оказаться плачевным, так как не ясно, сможет ли этот всеобъемлющий механизм справиться со всеми задачами с той легкостью и простотой, которая присуща универсальности и закреплению типов.