- •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.1. Реализация итераторов
- •6.2.2. Использование итераторов
- •7.2. Абстракция данных
- •7.4. Генераторы
- •8.2.2. Обобщенность
- •9.1.3. Пример
- •9.6. Заключение
7.2. Абстракция данных
В этом разделе мы опишем некоторый подход для спецификации, использования и реализации абстракций данных при программировании на языке Паскаль. Этот подход основывается сугубо на соглашениях по программированию. В языке CLU, наоборот, — компилятор обеспечивает непосредственную поддержку этих абстракций. Данные соглашения разработаны в основном для того, чтобы показать, что мы можем реализовать абстракции данных, не разрабатывая повторно какие-либо модули, которые их используют, и что можем возвращать из функций абстрактные значения.
На рис. 7.1 показана спецификация абстракции данных для очереди целых чисел. В этой абстракции задается тип intqueue и набор операций, которые по соглашению допускаются для работы с представлением этого типа. Имена всех операций начинаются с префикса q. Будем следовать этому соглашению, чтобы избежать конфликтов с именами операций, используемых для других типов данных.
Отметим, что, несмотря на тот факт, что обе операции q-ap-pend и q-remfirst формально модифицируют очередь q, сама очередь q не передается, как некоторый тип var. Мы реализуем тип данных intqueue и другие абстрактные типы, используя указатели на неупорядоченный массив. Когда мы передаем в процедуру некоторый абстрактный объект, то, следовательно всегда передаем некоторую ссылку. Чтобы модифицировать абстрактный объект, модифицируем тот объект, на который указывает ссылка, а не саму ссылку. Таким образом, абстрактные объекты никогда не передаются, как параметры типа var.
Наше решение хранить абстрактные объекты в неупорядоченном массиве продиктовано тем, что надо реализовать типы, чьи объекты изменяют свой размер по мере работы программы. Мы настаиваем на том, чтобы представление было некоторой ссыл кой, чтобы гарантировать то, что абстрактные объекты могут быть возвращены при помощи функций. Если бы, например, представление было некоторой записью, содержащей в качестве своей компоненты некоторый указатель, то его нельзя было бы вернуть при помощи функции. И наконец, использование указателя га-раш нрует эффективность при передаче абстрактных объектов по значению.
intqueue = data type is q_new, q.isempty, q_append,
q-remfirst
Описание
Типы данных intqueue используются для хранения значений типа целого числа. Элементы могут извлекаться из очереди или удаляться из нее только методом обратного магазинного типа («первым пришел — первым обслужен») Операции function q_new: intqueue
effects Возвращает новую очередь без элементов в ней. function qJsempty (q: intqueue): boolean
effects Возвращает значение true, если в очереди q нет элементов, в противном случае возвращает значение false. procedure q .append (q; intqueue; e: integer) modifies q
effects Добавляет элемент e в конец очереди q. function q_rernfirst (q: intqueue): integer requires чтобы очередь q была не пустой, modifies q effects Возвращает элемент из начала очереди q и удаляет этот элемент из
очереди q. end intqueue
Рис. 7.1. Спецификация очереди целых чисел.
По соглашению объявление типа представления всегда будет в такой форме:
type type: iai-ne == ^typename.rep;
где typename.rep является некоторым типом, который описывает «реальное» представление. Например, для типа intqueue мы используем такой тип представления:
type
intqueue = ^intqueue.rep; intqueue. rep = '[•intqLleue.elem; intqueue_elem == record val: integer; next: intqueue. rep; end;
Следуя нашим соглашениям, просто объявить переменные некоторого абстрактного типа, например,
var q: intqueue;
Смысл этого объявления состоит в том, что в стеке отводится пространство для некоторой неинициализированной переменной q. азмер этого пространства такой, чтобы поместился некоторый указатель, как в языке CLLJ.
Когда очередь q объявлена, выделяется только указатель, и, следовательно, когда организуется выход из процедуры, в которой объявлена очередь q, освобождается только этот указатель. Место для элементов очереди q выделяется в операциях типа intqueue при помощи явных обращений к примитиву new языка Паскаль. Поскольку в языке Паскаль сборка мусора автоматически не осуществляется, пространство, занятое этими элементами, должно быть явно освобождено той программой, в которой используется данная очередь. Для этой цели в языке Паскаль имеется примитив dispose. Указатель задается как аргумент для этого примитива, он следует этому указателю и освобождает то пространство, на которое указывает данный указатель. Размер освобождаемого пространства зависит от типа указателя. Примитив dispose (q) будет освобождать пространство, занятое одним указателем типа intqueue.rep. Это означает, что вся память, занимаемая элементами, будет оставаться занятой, и это может привести к особенно неприятной ошибке в программе, называемой «утечкой памяти». Утечка памяти происходит тогда, когда не была освобождена память, которая должна быть освобождена. Единственным симптомом этой ошибки является то, что через некоторое время неупорядоченный массив постепенно исчерпывается. К тому времени, когда в программе кончится такое пространство, может выполниться очень много команд от того места, где произошла сама утечка.
Чтобы гарантировать правильное освобождение пространсчЕа, для каждого типа дожна существовать некоторая дополнительная операция, которая называется destroy. Операция destroy использует один аргумент — некоторый объект данного типа, и она освобождает все пространство в неупорядоченном массиве, связанное с этим объектом. Значение данного объекта при возврате из операции destroy не определено. Приведем спецификацию процедуры q.destroy.
procedure q. destroy (q: intqueue)
modifies q
efiects В неупорядоченном массиве освобождается вся память, занимаемая очередью q.
По соглашению пользователи никогда не освобождают абстрактные объекты непосредственно. Вмесго этого они обращаются к операции destroy для типа данного объекта. При этом соглашении размер освобождаемого пространства находится под контролем. При этом, однако, не гарантируется, что к освобожденному объекту не будет больше ссылок. Если такая ссылка существует, то после освобождения данного объекта она становится «повисающей ссылкой». Если делается попытка получить доступ к памяти через повисающую ссылку, то может быть прочитана бессмысленная информация. Еще хуже модификации, сделанные через повисающие ссылки. Они могут нарушить непротиворечивость других объектов. Хотя при реализации па языке Паскаль можно обнаружить повисающие ссылки, такое обнаружение трудоемко и поэтому обычно не делается. На рис. 7.2 приведена реализация типа данных intqueue, включая и операцию destroy.
{Объявление типа представления для типа данных intqueue) type
intqueue = ^intqueiie_rep; intqueue_rep == ^г^иеие.^ет; intqueue_e!em == record val: integtr; next: intqtieue_rep; end;
{Начало реализации операций для типа данных intqueue} tunction q_new: intqueue; var q; intqueue; begin new (q); qf :== nil; q_new := q end {функция q_new};
function qJsempty (q: intqueue): boolean; begin qJsempty := (q^ = nil) end {функция qJsempty};
procedure q_append (q: intqueue; e: integer); var last_elem, elem: intqueue_rep; begin new (elem); ^ет^.уа) := e; elernf.next .'== nil; if qJsempty (q) then q^ := elem else begin last_elem :== qf; while last.elernf.next () nil do
last_elem :== last.elemf.next; last.elem^.next :== elem end;
end {процедура q_append}; tunction q_remfirst (q: intqueue): Integer; var oldq: infqueue_rep; begin
if qJsempty (q) (He удовлетворяются требования предложения requires} then failure (' К фрикции q_remfirst обратились с пустой очередью") else begin q.rernfirst :== q^.val; oldq :== qf;
qt ••= qTt-"ext:
dispose (oldq) {Освободить пространство, занимаемое удаленным элементом} end
end {функция q_rennfirst); procedure q_destroy (q: intqlieue) •var next.elem, old_elem: intqueue_rep; begin next.elem :•=. <\\', while next_elem ( ) nil do begin old_elem := next_elem; next_elem :=г next_elem•l•. next; dispose (old^elem); end; dispose (q) end {процедура q_destroy}; {Конец реализации операций типа данных intqueue}
Рис. 7,2. Реализация операций типа данных intqueue.
Рис. 7.3. Очереди ql и q2 совместно используют одну и ту же очередь.
По нашему соглашению следует, что каждый абстрактный тип, который представляется некоторым указателем, гарантирует, что присваивание абстрактных значений имеет тот же самый смысл, что и присваивание на языке CLU. Это не эквивалентно тому присваиванию, которое существует в языке Паскаль для встроенных в него типов. На языке Паскаль, например, присваивание массива а 1: == а2 приводит к копированию массива а2. Последующие изменения в массиве а2 не влияют на массив а1. Если, с другой стороны, мы напишем ql: = q2, где ql и q2 являются очередями целых чисел типа intqueue, то будет копироваться только указатель, а очереди ql и q2 будут совместно использовать очередь intqueue.
Важно, чтобы это совместное использование правильно работало. Последующие изменения в очереди q2 должны отражаться в очереди q), и наоборот. Мы достигаем этого, употребляя еще один уровень косвенного указания в представлении. Вместо того чтобы указывать непосредственно на первый элемент очереди, заголовки очередей ql и q2 указывают на некоторый указатель, который уже указывает на первый элемент очереди (рис. 7.3). Это позволяет нам удалять первый элемент из очереди и отражать это для обеих очередей ql и q2. Такое косвенное указание могло бы быть также сделано посредством некоторой записи, а не просто указателя. Например, предположим, что мы решили хранить размер очереди в представлении (это было бы полезным, если бы очереди целых чисел intqueue имели операцию size). Тогда бы мы задали следующее:
intqueue_rep = record
size: integer;
first; lintqueue_ elem;
end
Рис. 7.4. Очереди ql и q2 не используют совместно одну и ту же очередь.
а совместное использование все равно правильно бы paбoтало.
Часто бывает полезным включить в "тип некоторую функцию копирования, например функцию q_copy с определением, приведенным в гл. 4. Функции копирования должны обеспечивать полную неразделяемую копию аргументов. Они могут быть использованы всегда, когда мы хотим выполнить присвоение обычным семантическим правилом языка Паскаль. Например, мы могли бы написать ql: = q_copy (q2), а не ql: = q2. На рис. 7.4 показано отсутствие совместного использования в том случае, когда очереди ql присваивается значение функции q.copy (q2). Надо иметь в виду, однако, что эти функции копирования не будут вызываться компиляторами как часть неявного присваивания, например, при обращении по значению.
И наконец, отметим, что наш метод не препятствует тому, чтобы программы получали доступ к представлению вне реализации некоторого типа. По нашему соглашению мы просто считаем такой доступ не по правилам.
7.3. Полиморфные абстракции
Параметры типа языка CLU позволяют нам построить полиморфные абстракции, т. е. абстракции, которые могут быть использованы для объектов различного типа, В языке Паскаль имеется некоторый встроенный полиморфизм. Генератор типа массивов, например, может быть использован для объявления массивов с различными границами и элементами различных типов. Однако в языке Паскаль почти не обеспечивается определенный пользователем полиморфизм. Конечно, это не остановит нас перед проектированием полиморфных абстракций. Спецификация таких абстракций достаточно очевидна. Мы используем синтаксис, аналогичный тому, который использовался для специфи кации параметрических абстракций при реализации на языке CLU. Пример параметрической очереди приведен па рис. 7.5.
queue = data type retype: type] is q [etype]_new, q [etype]Jsempty, q [etype]_append, q [etype]_remiirst, q [etype].destroy
Описание
Очереди используются для хранения значений типа данных etype. Элементы могут извлекаться из очереди или удаляться из нее только методом обратного магазинного типа («первым пришел — первым обслужен»).
procedure q ietype]_append (q: queue [etype]; e; etype)
modifies q
efiects Добавляет элемент e в конец очереди q.
end queue
Рис. 7.5. Частичная спецификация параметрической счереди.
Поскольку язык Паскаль не обеспечивает реализацию параметрических абстракций, мы опять полагаемся па соглашение по программированию. Реализация параметрической очереди может выглядеть совсем как реализация inna данных intqueue. Объявление типа представления приводится на рис. 7,6.
type
queue [etypeJ == I queue [etype ]_rep; queue [etype ]_rep == ]quew [etype]_eleiT]; queue [etype ]_e!em == record val: etype; next: queue [etype ]_rep; end;
Рис.. 7.6. Тип представления для реализации типа queue [ etype].
Эти объявления не верны в смысле языка Паскаль, поскольку они содержат такие неверные идентификаторы, как queue letype!. В реализацию этих операций мы вводим также синтаксические ошибки. Эти синтаксические ошибки предотвращают оттого, чтобы программисты случайно не использовали нереализованные параметрические абстракции. Реализация операций аналогична реализации, приведенной на рис. 7.2, за исключением того, что префикс q заменяем на префикс queue [etype!, а идентификаторы intqueue и integer—соответственно на queue letype] и etype. Реализация операции q letypeLappend приведена на рис. 7.7.
Прежде чем использовать в программе реализацию типа данных queue [etype], мы должны использовать редактор текстсв, чтобы заменить каждое вхождение текста queue [etype] на неко-чорый удобный незапрещенный идентификатор, например на intq. Кроме того, надо заменить все оставшиеся вхождения иден-тификачора etype на некоторый уже известный тип, например
procedure q [etype] .append (q: queue [etype); e: etype); var )ast_elem, elem: queue [etype ]_rep; begin new (elem); elem+.val := e; elernT.next := nil; if q fetypeLisempty (q) then <\\ := elem else beiin ]ast_e]em := q^; while last_eleml•.next () nil do
last_elem := iast.eleml.next; last.elernf.next := elem end
end {процедура q [etypeLappend};
Рис. 7.7. Реализация процедуры q [etype]. append.
type intq s= •l•intq_гep; intq.rep == •l•intq_elem^ intq_elem = record val: ihteger; next: intq_rep; end;
procedure intq.append (q: intq; e: integer); var last_elem, elem; irftq.rep; begin new (elem); elem•r.val := e; elernf.next :=: nil; if intqJsempty (q) then •l' := elem else begin last_elem :== q•r,^ while last_elemf.next () nil do last_elem ;= last_elem•l•. next; last.elernf.next :== elem end
:• end {процедура intq.append};
Рис. 7.8. Конкретная реализация типа данных queue [etypel»
на integer, и надо изменить имена операций, заменив префикс, например переименовав q [etype Lappend в intq.append. На рис. 7.8 показана конкретная реализация объявления типа, взятого из рис. 7.8 g использованием реализации процедуры из рио. 7.7.