ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 21.05.2021

Просмотров: 243

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

16


Лекция 12

3.5. Проверка типов

Для обсуждения проверки типов обобщим понятия операндов и выражений, чтобы включить в эти понятия подпрограммы и операторы присваивания. Будем считать вызовы подпрограмм выражениями, операндами которых являются их параметры. Символ при­сваивания будем рассматривать как бинарную операцию, а операндами будут l-переменная и r-выражение.

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

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

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

Проверка типов осложняется, если язык позволяет хранить в ячейке памяти в разное время выполнения программы величины разных типов. Это можно сделать, например, с помощью вариантных записей языков Ada и Pascal, оператора EQUIVALENCE языка FORTRAN, объединений языков C и C++. В этих случаях проверка типов, если она производится, должна быть динамической, кроме того, она требует, чтобы система под­держки выполнения программ хранила типы текущих значений, записанных в таких ячейках памяти. Таким образом, даже если в языках, подобных C и Pascal, все перемен­ные статически связаны с типами, не все ошибки определения типа будут выявлены при статической проверке типов.

3.6. Строгая типизация

Одной из идей «структурной революции» в программировании 1970-х годов была строгая типизация, признанная весьма полезной кон­цепцией. В программах на строго типизированном языке каждое имя имеет связанный с ним тип, известный во время компиляции. Основой такого подхода является статичность связывания всех типов. Его слабая сторона – игно­рирование того факта, что ячейка памяти, с которой связана некоторая переменная, в разное время может содержать вели­чины различных типов. Чтобы учесть эту возможность, можно определить строго ти­пизированный язык таким образом, чтобы в нем всегда могли обнаруживаться ошибки типов. Для этого требуется, чтобы типы всех операндов могли определяться либо во время компи­ляции, либо во время выполнения. Важность строгой типизации заключается в возмож­ности выявления всех неправильных употреблений переменных, приводящих к ошибкам определения типов. Строго типизированные языки позволяют также обнаружить (в про­цессе выполнения) использование величин некорректных типов в переменных, которые могут содержать величины нескольких типов.


Язык FORTRAN не принадлежит к строго типизированным языкам, поскольку в нем не проверяются соответствия между фактическими и формальными параметрами подпрограмм. Кроме того, использование оператора EQUIVALENCE между переменными различных типов позволяет переменной одного типа ссылаться на переменную другого ти­па.

Язык Pascal относится к почти строго типизированным языкам. Этим «почти» он обязан своей структуре вариантных записей, которые позволяют совмещать значения различных типов без возможности их проверки.

Язык Ada также почти строго типизирован. Хотя в нем содержимое вариантных записей динамически проверяется на наличие величин кор­ректных типов, он позволяет программистам нарушать правила проверки, вводя требование о временной отсрочке проверки для конкретного преобразования типов с помощью специальной системной функции UNCHECKED_CONVERSION.

Языки C и C++ не относятся к строго типизированным языкам, поскольку в них до­пускаются функции без проверки типа параметров. Кроме того, типы объединений в этих языках также не проверяются.

Строго типизированным является язык ML, правда, с некоторыми отличиями от императивных языков. В ML все типы переменных известны ста­тически из объявлений либо правил логического вывода типов (см. п.3.4.2.3).

Языки Java и C#, хотя они в значительной мере созданы на основе C++, являются строго типизированным в том же смысле, что и язык Ada. Типы могут приводиться явно, что может вызвать ошибку определения типа. Тем не менее, не существует неявных пу­тей, позволяющих ошибкам определения типа остаться незамеченными.

Правила приведения типов существенно влияют на результаты их проверки. Выражения в языке Pascal строго типизированы, но допускается использование арифметических операций с операндами различных типов. Например, возможна операция сложения вещественной и целой величин. Значение целого операнда приводится к числу с плавающей точкой, и в результате выполняется операция над числами с плавающей точкой. Такое, казалось бы, естественное приведение типов снижает результат строгой типизации – возможность выявления ошибок. Языки, в которых широко используется приведе­ние типов, например, FORTRAN, С и C++, значительно менее надежны, чем языки, в ко­торых приведение типов применяется нечасто, например, Ada. В языках Java и C# содержится существенно меньше возможностей приведения типов, чем в C++.

3.7. Совместимость типов

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


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

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

type

indextype = 1. .100; {ограниченный тип}

var

count: integer;

index : indextype;

Если бы в Pascal использовалась совместимость имен типов, то переменные count и index не были бы совместимы.

Совместимость структур типов более гибка, но сложнее реализуется. При определении совместимости имен типов сравниваются только имена двух типов, а при использовании совместимости структур типов – структуры двух типов. Выполнить второе сравнение не всегда лег­ко. (Рассмотрите, например, структуру данных, ссылающуюся на собственный тип – связный список.) При этом могут возникать дополнительные вопросы. Являются ли, например, два структурных типа совместимыми, если они имеют одинаковую структуру, но разные имена полей? Совместимы ли два одномерных масси­ва в программе на языке Pascal или Ada, если они содержат элементы одного типа, но различаются областью значений индекса: 0..10 и 1..11?

Еще одной трудностью, связанной с совместимостью структур типов, является то, что она не признает различий между типами, имеющими одинаковую структуру. Рассмотрим следующее объявление, которое могло бы появиться в программе на языке Pascal:

type

celsius = real;

fahrenheit = real;

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

В исходном определении языка Pascal явно не устанавливается, когда должна использоваться совместимость структур типов, а когда – совместимость их имен. Это вредно для мобильности программ, поскольку программа, корректная в одной системе реализации языка, может оказаться некорректной в другой. Стандарт язы­ка Pascal, созданный Международной организацией по стандартизации, явно устанавливает правила совместимости типов для данного языка, частично – по имени, частично – по структуре. В большинстве случаев используется структура, а имя типа приме­няется для формальных параметров и в некоторых других ситуациях. Рассмотрим, на­пример, следующие объявления:


type

typel = array [1..10] of integer;

type2 = array [1..10] of integer;

type3 = type2;

В этом примере типы type1 и type2 несовместимы, что свидетельствует об интерпретации совместимости имен типов. Однако тип type2 совместим с типом type3, из чего можно заключить, что эквивалентность имен типов не трактуется строго. Такая форма совместимости иногда называется эквивалентностью объявлений, поскольку при определении типа с помощью имени другого ти­па оба они являются совместимыми, несмотря на то, что они несовместимы по именам типов.

В языке Ada применяется совместимость имен типов, но при этом имеются две кон­струкции – подтипы и производные типы, которые позволяют устранить возни­кающие проблемы. Производным называется новый тип, основанный на некотором ранее определенном типе, с которым он несовместим, несмот­ря на то, что они имеют идентичную структуру. Производные типы наследуют все свой­ства родительских типов. Рассмотрим следующий пример:

type celsius is new FLOAT;

type fahrenheit is new FLOAT;

Переменные типов celsius и fahrenheit несовместимы, хотя и имеют идентичную структуру. Более того, переменные этих типов несовместимы ни с каким другим типом чисел с плавающей точкой. Исключением из правила являются только литеральные константы. Литеральная константа, например, 3.0 имеет тип универсальных действительных чисел и совместима с любым типом чисел с плавающей точкой. Производные типы также могут содержать огра­ничения диапазона родительского типа, наследуя при этом все его операции.

Подтип в языке Ada – версия существующего типа с, возможно, ограничен­ным диапазоном. Подтип совместим с породившим его типом. Рассмотрим следующее объявление:

subtype SMALL_TYPE is INTEGER range 0..99;

Переменные, имеющие тип SMALL_TYPE, совместимы с переменными типа INTEGER.

Правила совместимости типов в языке Ada более строги, чем соответствующие правила в тех языках, в которых широко практикуется приведение типов. Например, два операнда, вхо­дящие в операцию сложения в языке C, могут иметь практически любую комбинацию чи­словых типов этого языка. Один из операндов при этом приводится к типу другого. В Ada нет приведения типов операндов арифметического оператора.

В языке C применяется структурная эквивалентность для всех типов, за исключением структур (записей) и объединений, для которых используется эквивалентность объявлений. Правда, если две структуры или объединения определяются в двух различ­ных файлах, то используется эквивалентность структур типов.

В языке C++ используется эквивалентность имен типов. При этом отметим, что оператор typedef языков C и C++ не вводит новый тип. Он просто определяет новое имя для уже сущест­вующего типа.

Во многих языках переменные могут объявляться без использования имен типа, при этом соз­даются безымянные типы. Рассмотрим следующий пример из языка Ada:


A : array (1..10) of INTEGER;

В этом случае переменная A имеет безымянный, но неоднозначно определенный, тип. После объявления

В : array (1..10) of INTEGER;

переменные A и B будут принадлежать к безымянным, различным и несовместимым типам, хотя они имеют идентичную структуру. Множественное объявление

С, D : array (1..10) of INTEGER;

создаст два безымянных типа: один для переменной C, другой – для переменной D, не­совместимых между собой. Фактически эти объявления можно рассматривать как сле­дующие два объявления:

С : array (1..10) of INTEGER;

D : array (1..10) of INTEGER;

Однако в объявлении

type LIST__10 is array (1..10) of INTEGER;

C, D : LIST_10;

переменные C и D будут совместимыми.

Очевидно, что в языках, не позволяющих пользователям определять и называть типы, например FORTRAN и COBOL, эквивалентность имен применяться не может.

Возникновение таких объектно-ориентированных языков как C++, Java, C# подняло вопрос о новой концепции совместимости типов – совместимости объектов и ее связи с иерархией наследования.

3.8. Область видимости

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

Правила обзора данных в языке определяют, как появление имени связано с переменной. В частности, каким образом ссылки на переменные, объявленные вне выполняющейся в данный момент подпро­граммы или блока, связаны с их объявлениями и, вследствие этого, с их атрибутами. Таким образом, для написания или чтения про­грамм на данном языке необходимо полное знание этих правил.

Переменная является локальной в программной единице или блоке, если она там объявлена. В данном разделе программными единицами считаются главный программный модуль или подпрограммы. Единицы, подобные клас­сам языков C++, Java, C# здесь не рассмотрены. Нелокальными переменными программной единицы или блока называются переменные, которые видимы в этой программной единице или блоке, но не объявляются в них.

3.8.1. Статическая область видимости

В языке ALGOL 60 был введен метод связывания имен с нелокальными переменными, названный статическим обзором данных. Позже этот метод был позаимст­вован большинством императивных, а также и многими неимперативными языками. Исполь­зование статического обзора данных получило свое название из-за возможности статиче­ского (то есть до периода выполнения) определения области видимости любой переменной.

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