- •4.9.1. Изменяемость
- •4.9.2. Классы операций
- •4.9.3. Полнота
- •4.9.5. Операции egual, similar и copy
- •5. Исключительные ситуации
- •6.2.1. Сигнализация об исключительных ситуациях
- •5.2.2. Обработка исключительных ситуаций
- •5.2.3. Предложение resignal
- •6.2.4. Необрабатываемые исключительные ситуации
- •6.2.3. Предложение resignal
- •5.2.4. Необрабатываемые исключительные ситуации
- •6.2.1. Реализация итераторов
- •6.2.2. Использование итераторов
- •7.2. Абстракция данных
- •7.4. Генераторы
- •9.1.3. Пример
4.9.3. Полнота
Тип данных является полным, если он обеспечивает достаточно операций для того, чтобы все требующиеся пользователю работы с объектами могли быть проделаны с приемлемой эффективностью. Строгое определение полноты дать невозможно, хотя существуют пределы относительно того, насколько мало операций может иметь тип, оставаясь при этом приемлемым. Например, для наборов intset мы предоставили только операции create, insert и delete, и программы не могут получить никакой информации относительно элементов набора (так как не имеется наблюдателей). С другой стороны, если мы добавим к этим трем операциям только одну операцию size, то сможем иметь информацию об элементах набора (например, можем проверять на принадлежность, удаляя целые числа и анализируя, изменился ли размер), но тип в этом случае получится неэффективным и неприемлемым.
В общем случае абстракция данных должна иметь операции по крайней мере трех из четырех рассмотренных нами в предыдущем разделе классов. Она должна иметь примитивные конструкторы, наблюдатели и либо конструкторы (если она неизменяемая), либо модификаторы (если изменяемая).
Полнота типа зависит от контекста использования, т. е. тип должен иметь достаточно богатый набор операций для его предполагаемого использования. Если тип предполагается использовать в ограниченном контексте (таком, как, например, одна программа), то должно быть обеспечено достаточно операций только для этого контекста. Если тип предназначен для общего использования, желательно иметь богатый набор операций. Вот почему встроенные типы языка CLU имеют много операций.
Чтобы решить, имеет ли абстракция данных достаточно операций, установите прежде всего все, что пользователи могут пожелать делать с ней. Затем продумайте, как эти вещи могут быть осуществлены с использованием данного набора операций. Если что-то окажется слишком неэффективным или слишком громоздким (или и то и другое вместе), исследуйте, может ли этому помочь какая-нибудь дополнительная операция. Иногда существенное улучшение в производительности можно получить, просто открыв доступ к представлению. Например, мы можем исключить операцию member для наборов intset, так как эта операция может быть реализована вне типа при помощи других операций. Однако проверка на принадлежность набору — операция общего использования, и будет работать быстрее, если . выполняется внутри реализации. Следовательно, тип данных intset должен иметь эту операцию.
Может быть также слишком много операций в типе. В этом случае абстракция будет менее понятной, а ее реализация и ра-
Абстракции данных
бота с ней —более сложными. Введение дополнительных операций должно быть соотнесено с затратами на реализацию этих операций. Если тип является полным, его набор операций может быть. расширен процедурами, функционирующими вне реализации типа.
4.9,4. Аназиз типов данных
Чтобы показать, что программа, которая использует тип данных, корректна, мы должны проанализировать свойства объектов типа. Этот анализ осуществляется на абстрактном уровне, т. е. мы используем спецификацию типа и игнорируем его реализацию. Такой анализ подробно обсуждается в гл'. II.
Например, иногда полезно установить свойство, являющееся общим для всех объектов типа. Такое свойство называется инвариантом абстракции — оно по существу является абстрактным аналогом инварианта представления. Такое свойство подобно теореме о целых числах и, как и теорема, доказывается с помощью индукции. Чтобы установить инвариант абстракции, мы должны показать, что он сохраняется для всех объектов, произведенных типом. Так как объекты производят только примитивные конструкторы, конструкторы и модификаторы, мы можем использовать следующий метод.
1. Показать, что свойство сохраняется для всех объектов, возвращаемых примитивными конструкторами.
2. Показать, что свойство сохраняется для объектов, возвращаемых конструкторами, предполагая, что свойство сохраняется для объектов, передаваемых конструкторам в качестве аргументов.
3. Показать, что при выходе из модификатора свойство сохраняется для модифицированных объектов, предполагая, что свойство сохраняется при вызове модификатора.
Например, интересное свойство упорядоченных списков (см. рис. 4.6) заключается в том, что они не содержат дублирующих друг друга элементов (что определяется операцией t$equal). Этот инвариант полезен для определения корректности реализации наборов intset с использованием упорядоченных списков. То, что это свойство является инвариантом, устанавливается следующим образом: 1) оно сохраняется для единственного примитивного конструктора create, так как операция create возвращает новый пустой список; 2) оцо сохраняется для операции addel (s, х). При возврате из этой операции в s содержатся те же элементы, которые там были до вызова, плюс элемент х. По предположению, до вызова в s нет дублирующих друг друга элементов. Кроме того, операция addel требует (в предложении requires), чтобы х не содержался в s. Следовательно, набор s с добавленным элементом х не содержит дублирующих друг друга элементов; 3) оно сохраняется для операции remel (s, х), так как оно сохраняется 4*
100 Глава 4
для s (по предположению), а операция remel только удаляет элементы из s.
Этот способ анализа называется индукцией типа данных. Индукция осуществляется по количеству вызовов процедур, используемых для получения текущего значения объекта. Первый шаг индукции — это установить сохранение свойства для примитивных конструкторов. Индукционный шаг устанавливает сохранность свойства для конструкторов и модификаторов.
Отметим, что анализ осуществляется на абстрактном уровне, а не на уровне реализации. Нас не интересует, как реализованы упорядоченные списки. Вместо этого мы работаем прямо с их спецификацией. Работа на абстрактном уровне существенно упрощает анализ.
Индукция также используется для того, чтобы показать, что реализации операций сохраняют инвариант представления (см. разд. 4.5.3). Однако этот анализ производится на уровне реализации. Как и в нашем случае, первый шаг — это установить сохранение свойства для примитивных конструкторов. Если представление изменяемое, на индукционном шаге необходимо рассматривать все операции, так как любая из них может изменить представление. Если же представление неизменяемое, то на индукционном шаге необходимо рассмотреть только конструкторы.