Сильно типизированный язык
Ада - это строго типизированный язык. Удивительно, как она современна в этом: сильная статическая типизация становится все более популярной в дизайне языков программирования, если судить по таким факторам, как развитие функционального программирования со статической типизацией, прилагаемые усилия в области типизации со стороны исследовательского сообщества и появление множества практических языков с сильными системами типов.
Что такое тип?
В статически типизированных языках тип в основном (но не только) является конструкцией времени компиляции. Это конструкция, обеспечивающая инварианты поведения программы. Инварианты - это неизменяемые свойства, которые сохраняются для всех переменных данного типа. Их применение гарантирует, например, что значения переменных данного типа никогда не будут иметь недопустимых значений.
Тип используется для описания объектов, которыми управляет программа (объект это переменная или константа). Цель состоит в том, чтобы классифицировать объекты по тому, что можно с ними сделать (т.е. по разрешенным операциям), и, таким образом, судить о правильности значений объектов.
Целочисленные типы - Integers
Приятной возможностью языка Ада является то, что вы можете определить свои собственные целочисленные типы, основываясь на требованиях вашей программы (т.е. на диапазоне значений, который имеет смысл). Фактически механизм определения типов, который предоставляет Ада, лежит в основе предопределённых целочисленных типов. Таким образом, в языке нет «магических» встроенных типов, как в большинстве других языков, и это, пожалуй, очень элегантно.
Этот пример иллюстрирует объявление целочисленного типа со знаком, и несколько моментов, связанных с его использованием.
Каждое объявление типа в Аде начинается с ключевого слова type
(кроме задачных типов). После ключевого слова
мы можем видеть определение нижней и верхней границ типа
в виде диапазона,
который очень похож на диапазоны используемые в циклах for
.
Любое целое число
этого диапазона является допустимым значением для данного типа.
Целочисленные типы Ада
В Аде целочисленный тип задается не в терминах его машинного представления, а скорее его диапазоном. Затем компилятор сам выберет наиболее подходящее представление.
Еще один момент, который следует отметить в приведенном выше примере,
- это выражение My_Int'Image (I)
. Обозначение вида
Name'Attribute (необязательные параметры)
используется для того,
что в Аде называется атрибутом.
Атрибут - это встроенная операция над типом, значением или какой-либо
другой программной сущностью. Доступ к нему осуществляется с помощью
символа '
(апостроф в ASCII).
Ада имеет несколько "встроенных" типов; Integer
-
один из них. Вот как тип целых чисел Integer
может быть определен
для типичного процессора:
type Integer is
range -(2 ** 31) .. +(2 ** 31 - 1);
Знак **
обозначает возведение в степень, в итоге, первое допустимое
значение для типа Integer
равно -231, а последнее
допустимое значение равно 231 - 1.
Ада не регламентирует диапазон встроенного типа Integer
.
Реализация для 16-битного целевого процессора, вероятно, выберет диапазон
от - 215 до 215 - 1.
Семантика операций
В отличие от некоторых других языков, Ада требует, чтобы операции с целыми числами контролировали переполнение.
Существует два типа проверок переполнения:
Переполнение на уровне процессора, когда результат операции превышает максимальное значение (или меньше минимального значения), которое может поместиться в машинном представлении объекта данного типа, и
Переполнение на уровне типа, если результат операции выходит за пределы диапазона, определенного для типа.
В основном по соображениям эффективности, переполнение уровня типа будет проверяться лишь в определенные моменты, такие как присваивание значения, тогда как, переполнение низкого уровня всегда приводит к возбуждению исключения:
Переполнение уровня типа будет проверяться только в определенных точках выполнения. Результат, как мы видим выше, состоит в том, что у вас может быть операция в промежуточном вычислении, которая переполняется, но никаких исключений не будет, пока конечный результат не вызовет переполняения.
Беззнаковые типы
Ада также поддерживает целочисленные типы без знака. На языке Ада они называются модульными типами. Причина такого обозначения связана с их поведением в случае переполнения: они просто "заварачиваются", как если бы была применена операция по модулю.
Для модульных типов машинного размера, например модуля 2 32, это имитирует наиболее распространенное поведение реализации беззнаковых типов. Однако преимущество Ада в том, что модуль может быть произвольным:
В отличие от C/C++, такое поведение гарантировано спецификацией языка Ада и на него можно положиться при создании переносимого кода. Кроме того, для реализации определенных алгоритмов и структур данных, таких как кольцевые буферы, очень удобно иметь возможность использовать эффект "заворачивания" на произвольных границах, ведь модуль не обязательно должен быть степенью 2.
Перечисления
Перечислимые типы - еще одна особенность системы типов в Аде. В отличие от перечислений C, они не являются целыми числами, и каждый новый перечислимый тип несовместим с другими перечислимыми типами. Перечислимые типы являются частью большего семейства дискретных типов, что делает их пригодными для использования в определенных ситуациях, которые мы опишем позже, но один контекст, с которым мы уже встречались, - это оператор case.
Типы перечисления достаточно мощные, поэтому, в отличие от большинства языков, они используются для определения стандартного логического типа:
type Boolean is (False, True);
Как упоминалось ранее, каждый "встроенный" тип в Аде определяется с помощью средств, обычно доступных пользователю.
Типы с плавающей запятой
Основные свойства
Как и большинство языков, Ада поддерживает типы с плавающей запятой.
Наиболее часто используемый тип с плавающей запятой - Float
:
Приложение отобразит 2.5
как значение A
.
Язык Ада не регламентирует точность (количество десятичных цифр в мантиссе) для Float; на типичной 32-разрядной машине точность будет равна 6.
Доступны все общепринятые операции, которые можно было бы ожидать для типов с плавающей запятой, включая получение абсолютного значения и возведение в степень. Например:
Значение A
равно 2.0
после первой операции и 5.0
после второй операции.
В дополнение к Float
, реализация Ада может предлагать типы данных с более
высокой точностью, такие как Long_Float
и Long_Long_Float
.
Как и для Float, стандарт не указывает требуемую точность этих типов:
он только гарантирует, что тип Long_Float
, например,
имеет точность не хуже Float
. Чтобы гарантировать необходимую
точность, можно определить свой пользовательский тип с
плавающей запятой, как будет показано в следующем разделе.
Точность типов с плавающей запятой
Ада позволяет пользователю определить тип с плавающей запятой с заданной точностью, выраженной в десятичных знаках. Все операции этого типа будут иметь, по крайней мере, заданную точность. Синтаксис простого объявления типа с плавающей запятой:
type T is digits <number_of_decimal_digits>;
Компилятор выберет представление с плавающей запятой, поддерживающее требуемую точность. Например:
В этом примере атрибут «'Size
» используется для получения количества бит,
используемых для указанного типа данных. Как видно из этого примера,
компилятор выделяет 32 бита для T3
, 64 бита для T15
и 128 битов
для T18
. Сюда входят как мантисса, так и экспонента.
Количество цифр, указанное в типе данных, также используется в формате при отображении переменных с плавающей точкой. Например:
Как и ожидалось, приложение будет отображать переменные в соответствии с заданной точностью (1.00E + 00 и 1.00010000000000000E + 00).
Диапазон значений для типов с плавающей запятой
В дополнение к точности для типа с плавающей запятой можно также
задать диапазон. Синтаксис аналогичен записи для целочисленных
типов данных — с использованием ключевого слова range
.
В этом простом примере создается новый тип с плавающей запятой на
основе типа Float
с диапазоном от -1.0
до 1.0
:
Приложение отвечает за обеспечение того, чтобы переменные этого типа
находились в пределах этого диапазона; в противном случае возникает
исключение. В следующем примере Constraint_Error
исключения
возникает при присваивании значения 2.0
переменной A
:
Диапазоны также могут быть заданы для пользовательских типов с плавающей запятой. Например:
В этом примере мы определяем тип под названием T6_Inv_Trig
, который имеет
диапазон от −𝜋/2 до 𝜋/2 с минимальной точностью 6 цифр. (Pi
определяется
в предопределенном пакете Ada.Numerics
.)
Строгая типизация
Как отмечалось ранее, язык Ада строго типизирован. В результате разные типы одного семейства несовместимы друг с другом; значение одного типа не может быть присвоено переменной другого типа. Например:
Следствием этих правил является то, что в общем случае выражение
«смешанного режима» типа 2 * 3.0
инициирует ошибку компиляции. В языке, таком
как C или Python, такие выражения допустимы благодаря
неявным преобразованиям типов. В Ада такие преобразования должны
быть явными:
Конечно, мы, вероятно, не хотим писать код преобразования каждый раз, когда мы преобразуем метры в мили. Идиоматическим решением в этом случае считается введение функции преобразования вместе с типами.
Если вы пишете код, где много вычислений, то необходимость явных преобразований может показаться обременительным. Однако такой подход дает определенные преимущества. Ведь вы можете полагаться на отсутствие неявных преобразований, которые, в свою очередь, могут привести к тяжело обнаружимым ошибкам.
На других языках
В C, например, правила для неявных преобразований не всегда могут быть полностью очевидными. Однако в Аде код всегда будет делать именно то, что, явно определено программистом. Например:
int a = 3, b = 2;
float f = a / b;
Этот код будет компилироваться нормально, но результатом f
будет 1.0
вместо 1.5, потому что компилятор сгенерирует целочисленное деление
(три, разделенное на два), что приведет к единице. Разработчик
программного обеспечения должен знать о проблемах преобразования
данных и использовать соответствующее приведение типов:
int a = 3, b = 2;
float f = (float)a / b;
В исправленном примере компилятор преобразует обе переменные в соответствующее представление с плавающей запятой перед выполнением деления. Что даст ожидаемый результат.
Этот пример очень прост, и опытные разработчики C, вероятно, заметят и
исправят его, прежде чем это создаст большие проблемы. Однако в более
сложных приложениях, где объявление типа не всегда видно - например,
при ссылке на элементы структуры struct
, - эта ситуация может не всегда быть
очевидной и быстро привести к дефектам программного обеспечения,
которые обнаружить может быть сложнее.
Компилятор Ада, напротив, всегда будет отклонять код, который смешивает переменные с плавающей запятой и целочисленные переменные без явного преобразования. Следующий Ада код, основанный на ошибочном примере в C, не будет компилироваться:
Строка с ошибкой должна быть изменена на F := Float (A) / Float (B);
.
Вы можете использовать строгую типизацию Ада, чтобы обеспечить соблюдение инвариантов в вашем коде, как в приведенном выше примере: поскольку мили и метры - это два разных типа, вы не можете случайно использовать значения одного типа вместо другого.
Производные типы
В Ада можно создавать новые типы на основе существующих. Это очень полезно: вы получаете тип, который имеет те же свойства, что и некоторый существующий тип, но ведет себя как отдельный тип в соответствии с правилами сильной типизации.
Тип Social_Security
, как говорят, является производным типом;
его родительский тип - Integer.
Как показано в этом примере, вы можете уточнить допустимый диапазон значений при определении производного скалярного типа (такого как целое число, число с плавающей запятой и перечисление).
Синтаксис перечислений использует синтаксис range <диапазон>
:
Подтипы
Вышеизложенное может привести нас к идее, что типы в Аде могут быть использованы для наложения ограничений на диапазон допустимый значений. Но иногда бывает нужно ограничить значения оставаясь в пределах одного типа. Здесь приходят на помощь подтипы. Подтип не вводит новый тип.
Несколько подтипов предопределены в стандартном пакете Ада и автоматически доступны вам:
subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;
Хотя подтипы одного типа статически совместимы друг с другом, ограничения проверяются во время выполнения: если вы нарушите ограничение подтипа, будет возбуждено исключение.
Подтипы в качестве псевдонимов типов
Ранее мы видели, что мы можем создавать новые типы, объявляя
type Miles is new Float
. Но нам также может потребоваться переименовать
тип, просто чтобы ввести альтернативное имя-псевдоним для
существующего типа.
Следует отметить, что псевдонимы типов иногда называются
синонимами типов.
В Аде это делается с помощью подтипов без новых ограничений. Однако в этом случае мы не получаем всех преимуществ строгой типизации Ады. Перепишем пример, используя псевдонимы типов:
В приведенном выше примере тот факт, что и метры (Meters
), и мили (Miles
)
являются подтипами Float
, позволяет нам смешивать переменные обоих типов
без преобразования типов. Это, однако, может привести к всевозможным
ошибкам в программировании, которых мы стремимся избежать, как
можно видеть в необнаруженной ошибке, выделенной в приведенном выше
коде. В этом примере ошибка в присвоении значения в метрах переменной,
предназначенной для хранения значений в милях, остается
необнаруженной, поскольку и метры (Meters
), и мили (Miles
)
являются подтипами Float
.
Поэтому, для случаев, подобных приведенному выше, рекомендуется использовать
строгую типизацию, опеделив тип X производным от типа Y (type X is new Y
).
Однако существует много ситуаций, где псевдонимы типов полезны. Например, в приложении, которое использует типы с плавающей запятой в нескольких контекстах, мы могли бы использовать псевдонимы типов, чтобы уточнить преднозначение или избежать длинных имен переменных. Например, вместо того, чтобы писать:
Paid_Amount, Due_Amount : Float;
Мы можем написать:
subtype Amount is Float;
Paid, Due : Amount;
На других языках
Например, в C для создания псевдонима типа можно использовать
объявление typedef
. Например:
typedef float meters;
Это соответствует определению подтипа без ограничений, которое мы видели выше. Другие языки программирования вводят эту концепцию аналогичными способами. Например:
C++:
using meters = float;
Swift:
typealias Meters = Double
Kotlin:
typealias Meters = Double
Haskell:
type Meters = Float
Однако следует отметить, что подтипы в Аде соответствуют псевдонимам
типов, если и только если они не вводят новых ограничений. Таким
образом, если добавить новое ограничение к описанию подтипа, у нас
больше не будет псевдонима типа. Например, следующее объявление
не может считаться синонимом типа Float
:
subtype Meters is Float range 0.0 .. 1_000_000.0;
Рассмотрим другой пример:
subtype Degree_Celsius is Float;
subtype Liquid_Water_Temperature is
Degree_Celsius range 0.0 .. 100.0;
subtype Running_Water_Temperature is
Liquid_Water_Temperature;
В этом примере Liquid_Water_Temperature
не является псевдонимом
Degree_Celsius
, поскольку добавляет новое
ограничение, которое не было частью объявления Degree_Celsius
.
Однако здесь есть два псевдонима типа:
Degree_Celsius
является псевдонимомFloat
;Running_Water_Temperature
является псевдонимомLiquid_Water_Temperature
, даже если самLiquid_Water_Temperature
имеет ограниченный диапазон.