Книга: Основы объектно-ориентированного программирования
Родовые классы
Согласование статической типизации с требованием повторного использования для классов, описывающих контейнерные структуры, означает, как показано на примере стека, что мы хотим одновременно иметь возможность:
[x]. Объявить тип каждой сущности, появляющейся в классе стека, включая сущности, представляющие элементы стека.
[x]. Написать класс так, чтобы он не содержал никаких намеков на тип элемента стека, и следовательно, мог использоваться для построения стеков с элементами произвольных типов.
На первый взгляд эти требования кажутся несовместимыми, но на самом деле это не так. Первое требование заставляет нас объявить тип. Но вовсе не требуется, чтобы тип в объявлении был конкретным! Назвав имя типа, мы умиротворим механизм проверки. ("Назови свой страх - и он уйдет"). В этом идея универсализации: получить класс с параметром, задающим тип, снабдить его именем, назвав его формальным родовым параметром.
Объявление родового класса
По соглашению родовой параметр обычно, использует имя G (от Generic). Это неформальное правило. Если нужны еще родовые параметры, они будут названы H, I и т.д.
Согласно синтаксису, формальные родовые параметры заключаются в квадратные скобки, следующие за именем класса, подобно синтаксису параметризованного АТД в предыдущей лекции. Например:
indexing
description: "Стек элементов произвольного класса G"
class STACK [G] feature
count: INTEGER
-- Количество элементов в стеке
empty: BOOLEAN is
-- Есть ли элементы?
do ... end
full: BOOLEAN is
-- Стек заполнен?
do ... end
item: G is
-- Вершина стека
do ... end
put (x: G) is
-- Втолкнуть x в стек.
do ... end
remove is
-- Вытолкнуть элемент из стека.
do ... end
end -- class STACK
Формальный родовой параметр G можно использовать в объявлениях класса не только для результата функций (как в item) и формальных аргументов подпрограмм (как в put), но и для атрибутов и локальных сущностей класса.
Использование родового класса
Клиент может использовать родовой класс для объявления собственных сущностей, задающих стек. В этом случае в момент объявления следует задать фактический тип элементов стека - фактический родовой параметр, например:
sp: STACK [POINT]
Если у класса несколько родовых параметров, то соответственно столько же необходимо задать и фактических параметров.
Предоставление фактических родовых параметров родовому классу для создания типа называется родовым порождением (generic derivation), а полученный в результате класс, такой как STACK [POINT], называют параметрически порожденным классом.
Родовому порождению требуется тип, родовое порождение создает новый тип:
[x]. Результат порождения STACK [POINT] является типом.
[x]. Для получения такого результата, необходим уже существующий тип, используемый в качестве фактического параметра (POINT в примере).
Фактический параметр может быть произвольным типом. Ничто не мешает выбрать тип, который сам по себе параметрически порожден. Предположим, что мы определили другой родовой класс LIST [G], тогда можно определить стек, элементы которого являются списками точек:
slp: STACK [LIST [POINT]]
или, используя STACK [POINT] как фактический родовой параметр, - стек стеков точек:
ssp: STACK [STACK [POINT]]
Нет предела глубины таких вложений, кроме естественной необходимости сохранять простоту программного текста.
Терминология
Обсуждая универсализацию, необходимо уточнить используемые термины.
[x]. Процесс порождения нового типа, такого как STACK [POINT], из типов POINT и STACK, можно было бы называть созданием экземпляра типа "generic instantiation". Но этот термин мог бы ввести в заблуждение, поскольку в названии неявно предполагается процесс периода выполнения ПО. Заметьте, родовое порождение - статический механизм, действующий на текст программы, а не на ее выполнение.
[x]. В этой книге термин "параметр" и "аргумент" используются по-разному. Первый для универсальных классов, второй - для подпрограмм. В традиционной программистской терминологии параметры и аргументы чаще всего синонимы.
Проверка типов
Используя универсализацию, можно гарантировать, что структура данных будет содержать элементы определенного типа. Допустим, класс содержит объявления:
sc: STACK [CIRCLE]; sa: STACK [ACCOUNT]; c: CIRCLE; a: ACCOUNT.
Тогда в программах этого класса допустимы следующие инструкции:
sc.put (c) -- Втолкнуть круг в стек кругов
sa.put (a) -- Втолкнуть счет в стек счетов
c := sc.item -- Сущности круг присвоить вершину стека кругов.
Но каждая из следующих инструкций недопустима и будет отвергнута:
sc.put (a); -- Попытка: Втолкнуть счет в стек кругов.
sa.put (c); -- Попытка: Втолкнуть круг в стек счетов.
c:= sa.item -- Попытка: Дать кругу значение счета.
Это исключает ошибочные операции, подобные попытке вычитания денег из круга.
Правило типизации
Правило типизации, делающее допустимым первый набор и недопустимым второй, интуитивно понятно, но его надо уточнить.
Вначале рассмотрим обычные, не родовые классы. Пусть C такой класс. Рассмотрим объявление его компонента, не использующее, естественно, никаких формальных родовых параметров:
f(a:T):U is ...
Тогда вызов вида x.f(d), появляющийся в произвольном классе B, где x типа C, будет корректен по типу, тогда и только тогда, когда:
[x]. f доступен классу B, - экспортирован всем классам или множеству классов, включающих B;
[x]. d принадлежит типу T. Если учитывать возможность наследования, то d может принадлежать потомкам T.
[x]. Результат вызова имеет тип U. В этом примере предполагается, что компонент f является функцией.
Теперь предположим, что C родовой класс с формальным родовым параметром G имеет компонент:
h (a: G): G is...
Вызов h имеет вид y.h(e), где y сущность, объявленная как
y: C [V]
Тип V - некоторый ранее определенный тип. Теперь правило типизации - двойник неродового правила - требует, чтобы e имело тип V или при наследовании было потомком V. Аналогичное требование к результату выполнения функции h.
Требования правила понятны: V - фактический параметр, заменяющий формальный родовой параметр G параметризованного класса C, поэтому он заменяет все вхождения G при вызове компонент класса. Все предыдущие примеры следовали этой модели: вызов s.put(z) требует параметра z типа POINT, если s типа STACK [POINT]; INTEGER если s типа STACK [INTEGER]; и s.item возвращает результат типа POINT в первом случае и типа INTEGER во втором.
Операции над сущностями родового типа
В родовом классе C [G, H, ...] рассмотрим сущность, чей тип - один из формальных родовых параметров, например x типа G. Когда класс используется клиентом для объявления сущностей, G, разумеется, может представлять любой тип. Поэтому любая операция, которую выполняют подпрограммы C над x, должна быть применима ко всем типам. Это ограничение позволяет выполнять только пять видов операций:
Использование сущностей формального родового типа
Корректно использовать сущность x, чей тип задан формальным родовым параметром G, можно следующим образом.
1 Слева от оператора присваивания x := y, где выражение y также имеет тип G.
2 Справа от оператора присваивания y := x, где сущность y также типа G.
3 В логических выражениях вида x = y или x /= y, где y также типа G.
4 Как фактический аргумент в вызове подпрограммы на месте формальных параметров типа G, или типа ANY.
5 Как цель вызова компонента класса ANY.
В частности, инструкция создания вида create x неприменима, так как нам ничего неизвестно о процедурах создания, если таковые есть, для класса, определенного возможным фактическим родовым параметром, соответствующим G.
Случаи (4) и (5) ссылаются на класс ANY. Упомянутый уже несколько раз, этот класс содержит компоненты, наследуемые всеми классами. Поэтому можно быть уверенным, что независимо от фактического типа G при родовом порождении компоненты будут доступны. Компонентами класса ANY являются все основные операции копирования и сравнения объектов: clone, copy, equal, deep_clone, deep_equal и др. Поэтому для x и y формального родового типа G корректно использовать следующие инструкции:
x.copy (y)
x := clone (y)
if equal (x, y) then ...
Случай (4) разрешает вызов a.f(x) в родовом классе C [G], если f имеет формальный аргумент типа G. В частности, возможна ситуация, когда a может быть типа D [G], где D другой родовой класс. В классе D [G] объявлен компонент f, требующий аргумент типа G, обозначающий в этом случае формальный родовой параметр класса D, а не класса С. (Если предыдущая фраза не совсем понятна, перечитайте ее еще раз, и, надеюсь, она покажется столь же прозрачной10.2), как горный ручей.)
Типы и классы
Мы уже научились смотреть на класс - центральное понятие объектной технологии, - как на продукт слияния двух концепций: модуля и типа. До введения универсализации можно было говорить, что класс - это модуль, но это и тип данных.
С появлением универсализации второе утверждение перестало быть буквально истинным, хотя нюанс невелик. Родовой класс, объявленный как C [G], является не типом, а шаблоном типа, задающим бесконечное множество возможных типов. Любой тип из этого множества можно получить, предоставив фактический родовой параметр, который, в свою очередь, является типом.
Это приводит к более общему и гибкому понятию. Но за выигрыш в мощности приходится немного пожертвовать простотой: только при небольшом насилии над языком можно продолжать говорить о "компонентах класса T" или о "клиентах T", если x объявлен, как имеющий тип T. Теперь T может быть параметрически порожденным типом C [U] из некоторого родового класса C и некоторого типа U. Конечно, основой типа остается родовой класс C, поэтому насилие над языком приемлемо.
Если требовать буквальной строгости, то терминология следующая. Любой тип T ассоциируется с базовым классом T, поэтому всегда можно говорить о компонентах и клиентах базового класса T. Если T неродовой класс, то он же является и базовым классом. Если T родовое порождение C [U, ...], то C является базовым классом T.
Базовые классы будут использоваться при введении еще одного вида типов, основанного также (как и все остальное в ОО-подходе) на классе, но косвенно: закрепленного типа (см. гл. 16.7). |
- Родовые АТД
- 9.1. Классы и прототипы
- Классы сертификатов
- Статические классы
- 9.8. Классы в ECMAScript 5
- 9.7.4. Иерархии классов и абстрактные классы
- Классы синхронизации, внедренные в версии .NET Framework 4.0
- Классы для программирования графики
- У14.6 Отложенные классы и прототип
- Совет 38. Проектируйте классы функторов для передачи по значению
- Классы обобщенных коллекций
- Диаграммы последовательности действий и граничные классы