Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования. Концепции и принципы

.pdf
Скачиваний:
2
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
Данные и типы
Для того мы и скрывали «сеть» в теле пакета, чтобы пользователь не мог напи
сать, например,
сеть(33).связан.число:= 7;
и нарушить тем самым дисциплину работы с сетью так, что последующее выпол нение процедуры
удалить(33);
(в которой есть цикл по массиву связей узла 33) может привести к непредсказуе мым последствиям.
Введя объявление (в), пользователь может нарушить целостность объекта
сеть1 оператором
сеть1(33).связан.число := 7;
Теперь читателю должна стать полностью понятной принципиальная важ
ность концепции регламентированного доступа к объектам класса «сети», упомя нутой при постановке задачи: нужно позволить пользователю создавать новые сети и работать с ними посредством объявленных операций, но нужно защитить сети от нежелательного (несанкционированного) доступа. Обратите внимание, раньше нежелательный доступ к объекту «сеть» был невозможен потому, что объект был невидим пользователю, скрыт в теле пакета. Объявив тип «сети» в спецификации, мы сделали видимыми для пользователя и имя типа, и имена (се лекторы) полей объектов этого типа.
Итак, основное противоречие – в том, что скрыть объявление типа «сети» в
теле пакета нельзя – ведь его нужно оставить видимым пользователю (чтобы он мог объявлять новые сети), а оставить это объявление полностью открытым так же нельзя – пользователь может нарушить целостность сетей.
Вот если бы ввести тип так, чтобы его имя было видимо, а строение объектов –
невидимо (то есть разделить его спецификацию и реализацию)! Тогда технологи ческая потребность в регламентированном доступе была бы полностью удовлет ворена.
Эта красивая и естественная идея в Аде воплощена в концепции ПРИВАТ
НЫХ типов данных (в Модуле2 – в концепции так называемых «непрозрачных» типов данных).
101
4.3.2. Приватные типы данных
Подумаем о выразительных средствах, реализующих концепцию регламентиро ванного доступа к данным определенного типа.
По существу, нужно определить некоторую абстракцию данных – проявить то,
что существенно (для пользователя – имя типа и операции с объектами этого типа), и скрыть то, что несущественно (и даже вредно знать пользователю – строе ние объектов). Но ведь аналогичная задача для операционных абстракций реша ется легко: в спецификацию пакета помещается то, что должно быть видимым (спецификация операции), а в тело – то, что следует скрыть (полное определение операции).
102
Современное состояние языков программирования
Так что было бы естественным аналогично оформить абстракцию данных – поместить в спецификацию пакета то, что должно быть видимым (что может иг рать роль минимальной «спецификации типа»), а в тело пакета упрятать полное определение типа.
В Аде минимальная «спецификация типа» воплощена конструктом «объявле ние приватного типа», например:
(a’) type ñåòè is private;
а также перечнем спецификаций применимых операций.
Полное определение приватного типа по аналогии с определениями операций кажется естественным поместить в тело пакета. (Именно так сделано в Модуле2.)
Почти так и нужно поступать в Аде. Но полное объявление приватного типа приходится помещать не в тело пакета, а в «полузакрытую» (ПРИВАТНУЮ) часть спецификации пакета, отделяемую от открытой части ключевым словом private.
Спецификация обновленного пакета, который назовем «управление_сетями», может выглядеть следующим образом:
package управление_сетями is ... – как и раньше; строки 2-11. type сети is private; ... – операции над сетями ... – строки 13-18. ... – строки 13'-18'. private
type запись_об_узле is
record
включен : bollean := false;
связан : связи; end record; type сети is array (узел) of запись_об_узле;
end управление_сетями;
В общем случае спецификация пакета имеет вид:
package имя_пакета is
объявления_видимой_части [private объявления_приватной_части ]
end имя_пакета;
Квадратные скобки указывают, что приватной части может и не быть (как, на
пример, в пакете управление_сетью).
Зачем же в Аде понадобилась приватная часть? Почему нет полной аналогии
между операционными абстракциями и абстракцией данных? (В языке Модула2 эта аналогия выдержана полностью.) На эти вопросы ответим позже.
Семантика приватной части проста. Эту часть можно считать «резидентом
тела пакета» в его спецификации. В теле пакета непосредственно доступны все имена, объявленные в спецификации пакета, в том числе и в приватной части.
Данные и типы
С другой стороны, в использующих этот пакет сегментах видимы только объявле ния открытой части спецификации.
Напишем один из таких использующих сегментов – процедуру две_сети:
with управление_сетями; use управление_сетями; procedure две_сети is сеть1, сеть2 : сети; begin
вставить(13, в_сеть => сеть1 ); вставить(33, в_сеть => сеть1 ); связать(13, 33, в_сети => сеть1);
сеть2 := сеть1; – присваивание полных объектов ! . . . end две_сети;
Когда управление дойдет до места, отмеченного многоточием, будут созданы две сети: «сеть1» и «сеть2», с узлами 33 и 13, причем эти узлы окажутся связанны ми между собой.
Таким образом, пользователи могут создавать сети и работать с ними с полной гарантией целостности – все требуемые услуги предоставлены нашим обновлен ным пакетом. Конечно, для этого нужно дополнить тело пакета, поместив туда определения новых операций. Оставим это в качестве упражнения.
Вопрос. Нельзя ли испортить сеть за счет того, что доступен тип «связи»? Ведь становится известной структура связей указанного узла.
Подсказка. А где средства для несанкционированного изменения этой структуры?
103
Итак, концепция регламентированного доступа в Аде воплощена разделением спецификации и реализации услуг (разделением спецификации и тела пакета), а также приватными типами данных.
Доступ к «приватным» данным возможен лишь посредством операций, объяв ленных в ОПРЕДЕЛЯЮЩЕМ ПАКЕТЕ. Для любого определяемого типа дан ных (не только приватного) так называется пакет, где расположено объявление этого типа данных (для типов «сети», «узел», «связи» и др. таковым служит пакет управление_сетями). Невозможен доступ, основанный на знании строения объектов (то есть выборкой или индексацией), – это строение скрыто в приватной части и в использующих сегментах неизвестно.
Подчеркнем, что в теле определяющего пакета объекты приватных типов ни чем не отличаются от любых других – их строение известно, выборка и индекса ция разрешены!
4.3.3. Строго регламентированный доступ. Ограниченные приватные типы
В общем случае к объектам приватных типов применимы также операции присва ивания и сравнения на равенство и неравенство (полных объектов, как в процеду ре две_сети!). Хотя это и удобно (такие операции часто нужны, и неразумно за
104
Современное состояние языков программирования
ставлять программистов определять их для каждого приватного типа), всетаки
концепция строго регламентированного доступа в таких типах не выдержана до
конца. В Аде она точно воплощена лишь в так называемых ОГРАНИЧЕННЫХ
приватных типах. К объектам таких типов неприменимы никакие предопределен
ные операции, в том числе присваивания и сравнения, – все нужно явно опреде
лять (в определяющем пакете). Объявления ограниченных приватных типов
выделяются ключевыми словами limited private, например:
type êëþ÷ is limited private;
Применение к объектам типа «ключ» операций присваивания или сравнения вызовет сообщение об ошибке, если только в определяющем пакете для этого типа не определены свои собственные операции, обозначаемые через «:=», «=» или «/=».
Так как ограниченные приватные типы точно воплощают идею строго регла ментированного доступа, интересно понять, в каких же задачах столь строгие ог раничения на присваивания и сравнения могут быть существенными.
Присваивания. Мы уже упоминали, что бывают ЯП вовсе без присваива ния. Таковы чистый Лисп, Базисный Рефал, функциональные и реляционные языки. Подробнее поговорим о них позже. Чтобы соответствовать роли базо вого языка, Ада должна предоставлять средства развития, позволяющие моде лировать и такие ЯП, причем моделировать в точности, с гарантией защиты создаваемых абстракций. Так что ограниченные приватные типы вместе со средством их определения (пакетом) – важнейшее средство развития в совре менном базовом ЯП.
Упражнение (повышенной сложности). Создайте определяющий пакет для каж дой из рассмотренных далее моделей ЯП.
Замечание (о наблюдении Дейкстры). Подтверждением принципа цельности (со гласованности данных, операций, связывания) служит наблюдение Дейкстры, что присваивание нужно лишь в тех языках, где есть повторения (циклы). Именно в циклах появляются переменные, которым нужно присваивать значения (посто янные относительно очередного исполнения тела цикла). Когда циклов нет и потенциальная бесконечность обслуживается рекурсией (в Лиспе, Рефале и т. п.), достаточна ИНИЦИАЛИЗАЦИЯ (частный случай присваивания – присваивание начальных значений). Так что наличие циклов должно быть согласовано не только с другими операциями (присваиванием), но и с данными (появляются переменные), а также со связыванием (областью действия каждой переменной в таких ЯП в идеале должен быть некоторый цикл).
Вопрос. Что можно сказать в этих условиях об исходных данных и результатах?
Рассмотрим пример. Допустим, что нужно моделировать выпуск изделий с уникальными номерами (вспомним заводские номера двигателей, автомобилей, самолетов и т. п.). Естественно считать, что объект приватного типа «изделие» – результат базовой для этого типа функции «создать», генерирующей очередное «изделие». Если позволить присваивать объекты этого типа переменным, то ста
Данные и типы
нет возможным их дублировать, и уникальность будет нарушена. Поэтому тип «изделие» должен быть ограниченным приватным.
Сравнения. Вопервых, операции сравнения для объектов некоторых типов
могут попросту не иметь смысла, то есть объекты могут быть несравнимыми. Из соображений надежности попытку их сравнения следует квалифицировать как ошибку (то есть сравнение должно быть запрещено). Так, при первоначальном знакомстве с Адой упоминались задачные типы, объекты которых – параллельно исполняемые задачи. Сравнение таких объектов на равенство (при отсутствии присваивания) бессмысленно просто потому, что любой из них уникален по опре делению. Да и содержательно трудно приписать какойлибо естественный смысл сравнению на равенство (а не, например, на подобие) двух независимо исполняе мых задач. Ведь они в постоянном изменении, причем асинхронном, а всякое ре альное сравнение занимает время.
Поэтому в Аде задачные типы – ограниченные приватные по определению, и,
следовательно, всякая попытка сравнить задачи на равенство квалифицируется как ошибка. Кстати, любые составные типы с компонентами ограниченного типа считаются ограниченными.
Вовторых, нежелание допускать сравнение на равенство может быть связано
с защитой от нежелательного доступа, с обеспечением секретности. Так, и пользо ватели, и файлы в файловой системе могут быть снабжены атрибутами типа «ключ». Однако неразумно разрешать пользователю сравнивать ключи, чтобы решать, пользоваться файлом или нет. Право сравнивать ключи и разрешать дос туп должно быть только у самой файловой системы. Поэтому для пользователя тип «ключ» должен быть ограниченным – он может его передать другому, но не может «подделать» или «подобрать».
105
4.3.4. Инкапсуляция
Определение приватных типов данных – один из примеров инкапсуляции – за ключения в «защитную оболочку», предохраняющую от разрушения. Мы видели, как недоступность (защита) строения объектов приватных типов от произвольно го доступа со стороны пользователя гарантирует их целостность в некотором содержательном смысле. Понятие инкапсуляции по общности занимает проме жуточное положение между концепцией регламентированного доступа и ее конк ретной реализацией приватными типами данных.
Приватные типы Ады с точки зрения пользователя – это инкапсулированные
(защищенные) типы данных. Однако с точки зрения реализатора тела определяю щего пакета – это обычные незащищенные типы. В общем случае инкапсулиро вать можно не только типы, но и отдельные объекты или системы объектов (тако вы объекты, объявленные в теле пакета).
Итак, задача о моделировании сетей помогла проявить технологическую по
требность в инкапсуляции и дала повод поработать с конструктами, удовлетво ряющими эту потребность, – с объявлениями приватного типа и пакетами.
106
Современное состояние языков программирования
4.4. Характеристики, связанные с типом. Класс значений, базовый набор операций
Продолжим анализ общей концепции типа данных. До сих пор мы концентриро вали внимание только на способе привязки характеристик к объектам данных. Сутью характеристик мы при этом не интересовались. Перед нами очередной пример разумной абстракции. Абстракция от сути характеристик привела к кон цепции уникальности типа. Концепция уникальности – к простоте прогнози рованияконтроля. А простота прогнозированияконтроля позволяет поновому взглянуть на саму концепцию типа.
Действительно, в ранних ЯП под характеристиками данных обычно понима лись характеристики их значений (уже упоминавшиеся разрядность, точность, вид выделяемой памяти и т. п.). Конечно, неявно всегда подразумевалось, что к значениям с одними характеристиками применимы одни операции, а к значени ям с другими характеристиками – другие. И раньше чувствовалось, что класс применимых операций – существенная содержательная характеристика класса данных (ведь это естественно следует из понятия данного как разумной абстрак ции от конкретной операции).
Но идею явного связывания класса данных с классом применимых операций трудно воплотить в рамках структурной эквивалентности типов. Ведь для опре деления класса данных, к которым применима операция, придется (как, напри мер, в Алголе68) производить нетривиальные расчеты совокупности данных, структурно эквивалентных параметрам операции.
Именная эквивалентность в концепции уникальности упростила связывание с типом любых характеристик. Легко узнать и классы данных, связанных с любой операцией, – имена типов явно указаны в спецификациях ее параметров.
Может показаться, что нетрудно проделать и обратное – указать для каждого типа все применимые операции. Но представим себе, что пакет управление_сетя ми предоставлен в распоряжение большого коллектива пользователей. Каждый из них волен написать сегменты, использующие этот пакет, и в этих сегментах ввести свои операции над объектами типа «сети». Например, один ввел процедуру выделения связных подсетей, другой – процедуру подсчета хроматического числа сети, третий – вычисления минимального маршрута между двумя узлами. Следу ет ли считать характеристикой типа «сети» набор из всех этих операций? И каков содержательный смысл в такой характеристике? Ведь в разных контекстах дос тупны разные элементы этого набора.
Лучше считать характеристикой типа относительно постоянное его свойство, связанное с ним в любых контекстах. Таковы КЛАСС ЗНАЧЕНИЙ объектов это го типа и БАЗОВЫЙ НАБОР ОПЕРАЦИЙ, применимых к этим объектам. В Аде эти две характеристики связываются с типом соответственно ОБЪЯВЛЕНИЕМ ТИПА и ОПРЕДЕЛЯЮЩИМ ПАКЕТОМ.
Данные и типы
107
4.5. Воплощение концепции уникальности типа. Определение и использование типа в Аде (начало)
Концепция типа воплощена в Аде в основном четырьмя конструктами: объявле нием типа, пакетом, объявлением подтипа и объявлением объекта. В совокупно сти они и позволяют считать, что тип в Аде обладает, кроме имени, еще двумя важнейшими характеристиками – классом значений и набором применимых опе раций. С каждым из названных четырех конструктов мы уже встречались. Пого ворим подробнее об их строении, смысле и взаимодействии.
4.5.1. Объявление типа. Конструктор типа. Определяющий пакет
Объявление типа вводит имя нового типа и связывает это имя с конструктором типа. Последний служит для создания нового типа из уже известных и располага ется в объявлениях типов после ключевого слова is.
Создать (объявить) новый тип в Аде – значит определить класс допустимых
значений объектов этого типа и набор базовых операций, связанных с создавае мым типом.
В Аде предопределены некоторые типы данных (обслуживающие наиболее
общие потребности проблемной области – универсальный_целый, универсаль ный_вещественный и др.), а также класс допустимых значений перечисляемых типов. Кроме того, предопределены операции, применимые к объектам целых ка тегорий типов.
Например, со всеми регулярными типами связана операция получения указа
теля на компоненту массива (индексация), со всеми комбинированными типами связана операция получения указателя на компоненту записи (выборка), со всеми так называемыми ДИСКРЕТНЫМИ типами – получение по заданному значе нию последующего или предыдущего значения. Если явно не оговорено обратное, то к объектам любого типа можно применять сравнение на равенство и нера венство, извлечение и присваивание значения.
Предопределенные типы, классы значений и операции служат исходным мате
риалом для нескольких категорий конструкторов типа – у каждой категории ти пов свой конструктор.
Объявление типа (а следовательно, и конструктор типа) служит для того, что
бы полностью и окончательно определить класс допустимых значений и (в общем случае лишь частично) определить базовые операции.
Полный и окончательный набор базовых операций некоторого типа фиксиру
ется его определяющим пакетом (так называется пакет, спецификация которого содержит объявление этого типа). В спецификации определяющего пакета вместе
108
с объявлением нового типа могут присутствовать и объявления новых операций, связанных с ним.
Например, конструктор производного типа, создавая тип «узел», определяет для него класс (в данном случае – диапазон) значений и связывает с типом «узел» обыч ные операции над целыми числами (наследуемые у родительского типа INTEGER). Остальные базовые операции для объектов типа «узел» (вставить, удалить, связать и др.) определены в конце спецификации пакета управление_сетью, который и служит для этого типа определяющим пакетом. Подчеркнем, что для типов, объявляемых в теле пакета, он, естественно, определяющим не считается (
Еще пример. Конструктор КОМБИНИРОВАННОГО типа, создавая тип «свя зи», определяет для него, вопервых, класс значений (записи с двумя полями – первое с СЕЛЕКТОРОМ «число» типа число_связей, второе с селектором «уз лы» типа «переченьсвязей»). Вовторых, этот конструктор определяет обычные для всех комбинированных типов операции доступа как ко всему значению объек та (по имени объекта), так и к его компонентам (по составным именам с использо ванием селекторов). Еще одна (и последняя) базовая операция для этого типа оп ределена в пакете управление_сетью – это операция все_связи. Доступ к полному значению объекта типа «связи» использован в реализации функции все_связи (в операторе возврата), а доступ к одному полю по селектору – в реализации операций вставить, удалить, чистить, переписать.
Конструктор ПРИВАТНОГО типа, создавая тип «сети», определяет в качест ве базовых операций только присваивание и сравнение на равенство и неравен ство. Определяющий пакет управление_сетями добавляет базовые операции вста вить, удалить и др.
Обратите внимание, одни и те же операции могут быть базовыми для различных типов! За счет чего?
Современное состояние языков программирования
почему?).
Класс значений для типа «сети» определен полным объявлением в приватной части, однако пользователю этот класс остается «неизвестным» (непосредствен но недоступным).
Запас предопределенных типов, значений и операций – это базис ЯП, а конст рукторы типа – характерный пример средств развития. Процесс развития ЯП (с помощью любых конструкторов) начинается с применения конструкторов к ба зису. На очередном шаге развития конструкторы применяются к любым уже оп ределенным сущностям (в том числе и к базисным).
4.6. Конкретные категории типов
4.6.1. Перечисляемые типы. «Морская задача»
Займемся технологической потребностью, которая часто встречается в так назы ваемых дискретных задачах (в дискретной математике вообще). Речь идет о по требности работать с относительно небольшими конечными множествами.
Данные и типы
Замечание (о конечных множествах). Когда конечные множества очень большие,
то с точки зрения программирования они могут оказаться неотличимыми от мно
жеств бесконечных. Специфика конечного сказывается тогда, когда мощность
множества удается явно учесть в программе. При этом в соответствии с принципом
согласования абстракций средства объявления конечных множеств должны быть
согласованы со средствами манипулирования как множествами в целом, так и от
дельными их элементами.
109
Рассмотрим очень упрощенную задачу управления маневрами корабля. Содержательная постановка. Корабль движется по определенному курсу. По
ступает приказ изменить курс. Требуется рассчитать команду, которую следует отдать рулевому, чтобы направить корабль по новому курсу (совершить нужный маневр).
Модель задачи. Будем считать, что возможных курсов только четыре: север,
восток, юг и запад. Можно отдать лишь одну из четырех команд: прямо, налево, направо и назад. Исполнение команд «налево» и «направо» изменяет курс на 90 градусов. Требуется рассчитать новую команду по заданным старому и новому курсам. Например, для старого курса «восток» и нового курса «север» нужный маневр выполняется по команде «налево».
Программирование (полная формализация задачи). Попытаемся вновь при
менить пошаговую детализацию.
Шаг 1. Нужно «рассчитать» команду. Это уже не комплекс услуг, а одна опера
ция. Представим ее функцией, для которой старый и новый курсы служат аргу ментами, а команда рулевому – результатом. Чтобы сразу написать специфика цию такой функции на Аде, нужно верить, что на последующих шагах можно будет ввести подходящие типы аргументов и результата. Поверим, не думая пока об особенностях этих типов. Тогда нужную операционную абстракцию можно воплотить следующей спецификацией:
function маневр(старый, новый : курс) return команда;
Шаг 2. Как формализовать понятие «команда»? С точки зрения ЯП, это должен быть тип данных – ведь имя «команда» ис
пользовано в спецификации параметров функции. Следовательно, нужно опреде лить связанные с этим типом значения и базовые операции.
С содержательной точки зрения ясно, что нужны только названия команд. Что
с ними можно делать – не ясно, это выходит за рамки модели нашей задачи (наша модель неполна в том отношении, что в ней отсутствуют, например, модель уст ройства корабля и модель рулевого как части корабля). Поэтому, формализуя по нятие «команда», удовлетворимся тем, что проявим список существенных команд и отразим в названиях команд их роль в управлении кораблем.
Существенных команд всего четыре: прямо, налево, направо, назад. Таким об
разом, нужно объявить тип данных с четырьмя перечисленными значениями. Ада позволяет это сделать с помощью следующего объявления ПЕРЕЧИСЛЯЕМО ГО типа:
type команда is(прямо, налево, направо, назад);
110
Современное состояние языков программирования
Не зря эта категория типов названа «перечисляемыми типами» – все допусти мые значения явно перечислены.
Такие типы называют иногда «перечислимыми». Однако этот термин в математике занят, относится тоже к множествам и имеет другой смысл (перечислимые множе ства вполне могут быть бесконечными). К тому же специфика рассматриваемых типов в том и состоит, что их значения явно перечисляют при определении такого типа.
Перечисляемые типы придуманы Н. Виртом и впервые появились в созданном им языке Паскаль. В наших примерах до сих пор были такие типы, для которых имело смысл говорить об описании множества значений, о создании нового или разрушении старого значения. Но никогда не шла речь о явном перечислении в программе всех значений некоторого типа. Другими словами, определения ти пов были всегда интенсиональными и никогда – экстенсиональными. Это было и не нужно, и, как правило, невозможно – шла ли речь о целых или вещественных числах, о массивах или записях. «Не нужно» означает, что такова была дисципли на применения этих типов. «Невозможно» связано с их практической бесконечно стью.
В нашем случае и можно, и нужно давать экстенсиональное определение типа, явно перечислив все значения типа «команда». Можно, потому что их всего четы ре, и мы их уже перечислили. А зачем это нужно?
Нужно потому, что всякое интенсиональное определение опирается на сущест венные индивидуальные свойства элементов определяемого множества. А в на шем случае таких свойств нет! Годятся любые различные имена для команд (же лательно мнемоничные, отражающие содержательную роль команд в нашей задаче). Захотим – будет {прямо, налево, направо, назад}, не понравится – сделаем {вперед, влево, вправо, обратно} или {так_держать, лево_руля, право_руля, зад ний_ход} и т. п. Поэтому нет иного способа догадаться о принадлежности конк ретного имени к типу «команда», кроме как увидеть его в соответствующем списке.
Итак, объявление перечисляемого типа вводит мнемонические имена для ком понент модели решаемой задачи (в нашем случае – имена команд). До изобрете ния Вирта программисты явно кодировали такие компоненты (обычно целыми числами). В сущности, Вирт предложил полезную абстракцию от конкретной ко дировки, резко повышающую надежность, понятность и модифицируемость про грамм без заметной потери эффективности.
Упражнение. Обоснуйте последнее утверждение. Подсказка. См. ниже стр. 113.
Шаг 3. Как формализовать понятие «курс»?
Нетрудно догадаться, что «курс» должен быть перечисляемым типом:
type курс is(север, восток, юг, запад);
Ведь снова, как и для типа «команда», с точки зрения решаемой задачи абсо лютно несущественно внутреннее строение этих значений. Поэтому невозможно вводить «курс» какимлибо конструктором составного типа. Действительно, из
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]