Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ещё одна методичка по ЛО.doc
Скачиваний:
18
Добавлен:
23.03.2016
Размер:
433.15 Кб
Скачать

2.2.3. Контроль типов

Тип данных на абстрактном уровне языка программирования можно рассматривать как факторизацию определенных свойств, являющихся общими для конкретного класса объектов. Если множество типов данных ограничено предопределенными типами, то все объекты, представленные, скажем, целым типом, по необходимости принадлежат одному и тому же классу просто потому, что они одного типа. Это, однако, может не соответствовать смыслу и использованию таких объектов в программе. Кроме этого, если все простые объекты данных в программе описаны с помощью предопределенных типов, то высокой надежности достичь не удается. В этом случае эффект сильной типизации не велик вследствие малого числа и широкого диапазона значений предопределенных типов. (Напомним, что основа надежности -- контроль типов с именной их эквивалентностью.) Чтобы компилятор мог эффективно выявлять типовые ошибки, программист должен вводить различные типы для обозначения данных логически разных классов.

Рассмотрим механизмы типизации и возникающие в связи с ним проблемы.

Начнем с проблем, решаемых использованием производных типов, на примере простой программы последовательного чтения значений возраста и вычисления суммарного возраста для группы из 10 человек:

procedure 2 ВОЗРАСТ1 is

use ТЕХТ_IO: package;

ЭТОТ_ВОЗРАСТ, ОБЩИЙ_ВОЗРАСТ: integer;

I: integer;

begin

ОБЩИЙ_ВОЗРАСТ:=0;

I:=1;

while 2  0(I<=10) do

GET(ЭТОТ_ВОЗРАСТ);

ОБЩИЙ_ВОЗРАСТ:=ОБЩИЙ_ВОЗРАСТ+ ЭТОТ_ВОЗРАСТ;

I:=I+1;

end do;

PUT(ОБЩИЙ_ВОЗРАСТ);

end procedure;

Здесь все три переменные ЭТОТ_ВОЗРАСТ, ОБЩИЙ_ВОЗРАСТ и I целого типа, но ясно, что они принадлежат к двум логически разным классам объектов. Если бы программист написал

ОБЩИЙ_ВОЗРАСТ:=ОБЩИЙ_ВОЗРАСТ+I;

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

procedure ВОЗРАСТ2 is

use ТЕХТ_IO: package;

type ВОЗРАСТ is integer;

ИНДЕКС is integer;

ЭТОТ, ОБЩИИ: ВОЗРАСТ;

I: ИНДЕКС;

begin

ОБЩИЙ:=0;

I:=1;

while 2  0(I<=10) do

GET(ЭТОТ);

ОБЩИЙ:=ОБЩИИ+ ЭТОТ;

I:=I+1;

end do;

PUT(ОБЩИИ);

end procedure;

определяются два новых типа данных ВОЗРАСТ и ИНДЕКС через тип integer. Описанные с помощью типа ВОЗРАСТ объекты помещаются явно в класс, логически отличный от класса объектов, описанных типом ИНДЕКС. Такие новые типы называют производными.

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

ОБЩИЙ:=ОБЩИИ+I;

Повышается также и ясность программы. Имя типа можно выбрать так, чтобы оно отражало смысл объектов этого типа.

Облегчается выбор имен переменных: в приведенной выше программе ВОЗРАСТ2 имена ОБЩИИ_ВОЗРАСТ и ЭТОТ_ВОЗРАСТ записаны в упрощенной форме, так как информация о том, что они содержат значения возраста, задается теперь в описании производного типа.

Определение типа ВОЗРАСТ, используемого для описания переменной ЭТОТ, в виде

type ВОЗРАСТ is integer;

имеет существенный с точки зрения обеспечения надежности недостаток: это определение слишком широкое. Ведь ясно, что возраст не может быть меньше нуля и больше, например, 200; кроме этого, с возрастом, очевидно, бессмысленно выполнять операции умножения, возведения в степень и т.п. Конечно, необходимо дать компилятору возможность проверять правильность значений возраста (т.е. принадлежность их указанному диапазону) и правильность выполнения операций над ними.

Для этого используются ограничения.

Описанный выше тип ВОЗРАСТ наследует от своего порождающего типа integer все его множество значений (а, следовательно, и предопределенные имена констант) и весь его набор операций. Ограничить наследуемое множество значений позволяют ограничения значений. Например, тип

type ВОЗРАСТ is integer range 0..200;

имеет в качестве значений целые числа от О до 200. Ограничить наследуемый набор операций позволяют ограничения операций. Например, тип

type ВОЗРАСТ is integer operations +, - , <, >, <=, >=;

допускает выполнение лишь указанных операций (а также, конечно, операций/=, которые наследуются всегда, и так как они входят и определение типа).

Теперь можно записать окончательный вариант программы:

procedure ВОЗРАСТ3 is

use ТЕХТ_IO: package;

type ВОЗРАСТ is integer range 0..200

operations +, - , <, >, <=, >=;

type СУММАРНЫЙ_ВОЗРАСТ is integer range 0..

operations +, - , <, >, <=, >=;

type ИНДЕКС is integer;

ЭТОТ: ВОЗРАСТ;

ОБЩИЙ : СУММАРНЫЙ_ВОЗРАСТ

I: НДЕКС;

begin

ОБЩИЙ:=0;

I:=1;

while(I<=10) do

GET(ЭТОТ);

ОБЩИИ:=ОБЩИИ 2+ 0СУММАРНЫЙ_ВОЗРАСТ(ЭТОТ);

I:=I+1;

end do;

PUT(ОБЩИИ);

end procedure;

Ограничения можно использовать и для других типов, отличных от целого, например

type ВЕРОЯТНОСТЬ is real range О. О .. 1. О;

type ЦИФРЫ is character range 'О' .. '9';

Необходимо лишь, чтобы на порождающем типе были определены операции <, >, <=, >=. В этом случае наследуемое множество значений указывается его минимальным и максимальным значениями (эти значения, конечно, должны принадлежать множеству значений порождающего типа).

Для действительных типов можно ограничивать также точность представления значений, например

type FLOAT is real digits 6;

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

Таким образом, все целые, действительные и символьные типы являются производными от универсальных типов integer, real и character.

Для производных типов можно не только ограничивать набор операций, но и вводить новые операции.

П р и м е р 2.6. Описание новой операции для производного типа:

type ВОЗРАСТ is integer range 0.. 200

operations +, -, <, >, <=, >=;

function "/" (В1, В2: ВОЗРАСТ) return (ОТН: integer) is

begin

ОТН:=integer(В1)/integer (В2);

end function;

ПОРОГ: integer:= 5;

ПЕРВЫИ, ВТОРОЙ: ВОЗРАСТ;

..............

if (ПЕРВЫИ/ВТОРОИ>=ПОРОГ) then

PUT(" Первый возраст значительно больше второго" );

end if;

Здесь используется специальная форма для имени функции, которая позволяет записывать вызов этой функции в виде выражения (вместо "/" (ПЕРВЫИ,ВТОРОИ), что также, конечно, возможно).

Заметим, что тип значения операции деления возрастов не есть ВОЗРАСТ (как это было бы, если бы набор операций не ограничивался): он целый, т.е. отражает наше представление о том, что отношение возрастов должно быть безразмерной величиной. Это позволяет компилятору самому обнаруживать такие ошибки, как в следующем, бессмысленном с интуитивной точки зрения, выражении

ПЕРВЫЙ+ ПЕРВЫЙ/ВТОРОЙ

──────────────────

Для правильного понимания механизма определения новых операций нужно ясно сознавать, что операция /, определенная для операндов типа ВОЗРАСТ в примере 2.6, отличается от операции /, определенной для целых операндов. В таких случаях говорят, что знак операции перегружен. Какая операция имеется в виду в конкретном контексте, определяется компилятором при исследовании типов операндов и предполагаемого результата.

Большинство рассмотренных операций являются перегруженными.

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

I integer;

ГОТОВО: boolean;

СУММА: real;

A: array (1..100) of real;

........

while (not ГОТОВО) do;

I:=I+1;

СУММА:=СУММА+ A(I);

ПЕРЕВЫЧИСЛИТЬ(ГОТОВО);

end do;

необходимо осуществить проверку истинности двух условий I>=1 и I<=100 перед выбором элемента массива. Если мы определим тип переменной I как

I : integer range 1..100;

то эта проверка будет производиться при обновлении значения

I в операторе

I:=I+1;

Но здесь нужно проверять только одно условие, а именно

(I+1)<=100

так как прибавление положительного числа к I не может вывести его за нижнюю границу диапазона. Никакой анализ текста не может установить, что проверка нижней границы массива не необходима : компилятор не располагает в первом случае какими-либо сведениями о значении I при входе в цикл. Эта априорная информация гарантируется во втором случае ограниченным диапазоном типа переменной I.

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

Для этого используются подтипы.

─────────────────

П р и м е р 2.7. Описание типа и подтипа:

type ДНИ_НЕДЕЛИ is (ПН,ВТ,СР,ЧТ,ПТ,СБ,ВС);

subtype РАБОЧИЕ_ДНИ is ДНИ_НЕДЕЛИ range ПН..ПТ;

X:РАБОЧИЕ_ДНИ;

Здесь введено не два типа, а один: переменная Х имеет тип ДНИ_НЕДЕЛИ , но ее значения ограничены диапазоном от значения ПН до значения ПТ. Объекты типа РАБОЧИЕ_ДНИ могут свободно смешиваться с объектами типа ДНИ_НЕДЕЛИ не требуя явного применения преобразования типа.

───────────────

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

Далее будем использовать два важных подтипа:

subtype natural is integer range 0.. integer'LAST;

subtype positive is integer range 1.. integer'LAST;

Отметим, что использовать подтипы следует лишь в действительно обоснованных случаях. Неограниченное их применение снижает возможности контроля типов и , следовательно, надежность. (Позже рассмотрим средства для описания подтипов структурных типов).

Используем механизм подтипов в программе подсчета суммарного возраста. В процедуре ВОЗРАСТ3 были введены два различных типа ВОЗРАСТ и СУММАРНЫИ_ВОЗРАСТ , хотя смысл их примерно один и тот же (только тип ВОЗРАСТ ограничен сверху).

Это привело к необходимости использовать явное преобразование типа. Можно исправить этот недостаток используя подтипы.

procedure ВОЗРАСТ4 is

use ТЕХТ_IO: package;

type СУММАРНЫЙ_ВОЗРАСТ is integer range 0..

operations +, - , <, >, <=, >=;

subtype ВОЗРАСТ is СУММАРНЫЙ_ВОЗРАСТ range 0..200

type ИНДЕКС is integer;

ЭТОТ: ВОЗРАСТ;

ОБЩИЙ : СУММАРНЫЙ_ВОЗРАСТ

I:ИНДЕКС;

begin

ОБЩИЙ:=0;

I:=1;

while(I<=10) do

GET(ЭТОТ);

ОБЩИИ:=ОБЩИИ+ ЭТОТ;

I:=I+1;

end do;

PUT(ОБЩИИ);

end procedure;

Часто встречаются ситуации, когда нет необходимости вводить имя нового типа -- будут описаны лишь один или два объекта этого типа. В этом случае можно записать определение типа в том месте, где требуется имя типа. Например, переменные Р и Q

type КЛЮЧ is (ВКЛ, ВЫКЛ);

Р, Q: КЛЮЧ;

можно описать, используя механизм анонимных типов:

Р, Q: (ВКЛ, ВЫКЛ);

Следует обратить внимание на эквивалентность типов. Безусловно, что в приведенном описании переменных Р и предполагается, что типы обеих переменных одинаковы. Когда Р и Q описаны раздельно, как

Р: (ВКЛ, ВЫКЛ);

Q: (ВКЛ, ВЫКЛ);

нет никаких оснований предполагать, что Р и Q одного и того же типа. На самом деле Р и Q должны рассматриваться как уникальные примеры двух различных типов, неэквивалентных никаким другим. Следовательно, например, оператор присваивания

Р:=Q;

считается незаконным. Более того, нет никакой функции преобразования, которую можно было бы применить для реализации таком присваивания.

Таким образом, можно считать, что использование анонимного типа вводит определение нового типа с некоторым уникальным, неизвестным программисту именем. Отсюда следует, что именная эквивалентность типов резко ограничивает практическую применимость механизма анонимных типов. И это неплохо, так как злоупотребление анонимными типами может существенно ухудшить удобочитаемость программ, уменьшить возможности контроля типов и тем самым снизить надежность программ. Ограничения на использование анонимных типов благоприятно сказываются на стиле программирования.