ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 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 был введен метод связывания имен с нелокальными переменными, названный статическим обзором данных. Позже этот метод был позаимствован большинством императивных, а также и многими неимперативными языками. Использование статического обзора данных получило свое название из-за возможности статического (то есть до периода выполнения) определения области видимости любой переменной.
Большинство отдельных статических областей видимости в императивных языках связаны с определениями программных единиц. Предположим, что все области видимости связаны с программными единицами. В данном разделе будем полагать, что для обращения к нелокальным переменным в обсуждаемых языках используются только области видимости. Последнее не совсем справедливо даже для языков со статическим обзором данных, но такое предположение упрощает обсуждение.