Роль типизации

 

Зачем настаивать на явном объявлении типов (первое из двух требований)? Это часть главного вопроса о типизации, которому в этой книге посвящена отдельная лекция (лекция 17). Но уже сейчас можно указать две основные причины, по которым ОО-программа должна быть статически типизирована.

[x]. Читаемость: явное объявление четко сообщает читателю о том, как предполагается использовать каждый элемент. Это важно как для автора, так и для того, кому нужно понять часть программы, чтобы отладить или расширить ее.

[x]. Надежность: благодаря явному объявлению типов компилятор сможет найти ошибочные операции еще на этапе компиляции, не допуская их проявления при выполнении. В фундаментальных операциях ОО-вычислений вызов компонента имеет форму x.f (a,..) , где х - некоего типа TX . Причины возникновения ошибок могут быть разными: соответствующий класс TX может не иметь метода f ; метод может существовать, но быть скрытым; количество аргументов при вызове может не совпадать с объявленным в описании класса; тип а или другого аргумента может не совпадать с ожидаемым. В языке Smalltalk, в котором отсутствует статическая типизация, любая такая ситуация приведет к краху на этапе выполнения с выдачей, например, сообщения: "Message not understood", в то время как компилятор языка с явной типизацией не пропустит ошибочной конструкции.

Ключ к надежности - следование принципу "предотвратить, а не лечить". Исследования показали, что стоимость исправления ошибки астрономически возрастает, когда затягивается ее обнаружение. Статическая типизация, позволяющая раннее обнаружение ошибок, - фундаментальный инструмент в борьбе за надежность.

Без учета требований надежности явное объявление типов было бы не нужно так же как универсализация. Остаток этой лекции обращается к языкам со статической типизацией, т.е. языкам, которые требуют объявления каждой сущности и задают правила, позволяющие компиляторам обнаруживать несоответствие типов до выполнения. В не статически типизированных языках, таких как Smalltalk, универсализация не имеет смысла. Язык упрощается, но не защищает от схем вида:

 

my_stack.put (my_circle)

my_account := my_stack.item

my_account.withdraw (5000)

 

 

где элемент, полученный из вершины стека, рассматривается как банковский счет, хотя в действительности это круг, что можно понять из первой инструкции. Выполнение программы закончится, при попытке получить пять тысяч долларов от "дырки от бублика".

Статическая типизация защищает от подобных неудач. Совмещение типизации с требованием повторного использования приведет нас к механизму универсализации.

 

 

Родовые классы

 

Согласование статической типизации с требованием повторного использования для классов, описывающих контейнерные структуры, означает, как показано на примере стека, что мы хотим одновременно иметь возможность:

[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]]

 

 

Нет предела глубины таких вложений, кроме естественной необходимости сохранять простоту программного текста.