Поддиапазоны и производные типы
Поддиапазоны и производные типы: как ограничить значения диапазоном прямо в типе, в чём разница между «совместимым» подтипом и «несовместимым» производным типом, и почему это решает реальные инженерные проблемы.
Поддиапазон (subtype) сужает множество значений существующего типа, оставаясь с ним совместимым; производный тип (derived type) создаёт совершенно новый, несовместимый тип на основе старого.
Объявление собственных типов
До сих пор мы пользовались готовыми типами вроде Integer. Но настоящая сила Ada — в том, что вы создаёте свои типы под конкретную задачу. Самый частый приём — целочисленный тип с заданным диапазоном:
type Day_Of_Month is range 1 .. 31;
type Temperature is range -273 .. 1_000;
type Percent is range 0 .. 100;
Конструкция type Имя is range Низ .. Верх; создаёт новый целочисленный тип с указанными границами. Две точки .. задают диапазон включительно. Теперь переменная типа Percent физически не может выйти за пределы 0–100: попытка присвоить 150 либо отвергается компилятором (если он это докажет статически), либо в рантайме поднимает Constraint_Error. Вы закодировали ограничение прямо в типе, и оно стало проверяемым законом, а не комментарием-пожеланием.
Поддиапазоны: сужение без разрыва родства
Иногда вы хотите ограничить значения, но при этом сохранить совместимость с базовым типом, чтобы свободно смешивать величины. Для этого служит подтип (subtype):
subtype Working_Hour is Integer range 0 .. 23;
subtype Adult_Age is Integer range 18 .. 150;
H : Working_Hour := 9;
N : Integer := 100;
R : Integer := H + N; -- ОК: Working_Hour совместим с Integer
Ключевое: Working_Hour — это тот же самый Integer, просто с дополнительным ограничением диапазона. Значения подтипа и базового типа взаимозаменяемы в выражениях — никаких преобразований не нужно. Подтип лишь добавляет проверку границ. Это и есть смысл слова «subtype»: подмножество значений того же типа. Кстати, знакомые Natural и Positive определены в стандарте именно так: subtype Natural is Integer range 0 .. Integer'Last; и subtype Positive is Integer range 1 .. Integer'Last;. Теперь понятно, почему их можно свободно смешивать с обычными целыми.
Производные типы: намеренная несовместимость
А теперь — противоположный инструмент. Иногда вы хотите создать тип, который, наоборот, нельзя случайно смешать с другими, даже если в основе тот же Integer. Это нужно, чтобы развести величины разной природы. Для этого служат производные типы через type ... is new:
type Meters is new Integer;
type Seconds is new Integer;
Distance : Meters := 100;
Duration : Seconds := 30;
Bad : Meters := Distance + Duration; -- ОШИБКА: Meters + Seconds запрещено
Вот это — одна из самых важных идиом надёжного программирования на Ada. Meters и Seconds оба «сделаны из» Integer, но это разные, несовместимые типы. Сложить метры с секундами невозможно — компилятор отвергнет бессмыслицу. Вы физически не сможете перепутать дистанцию и время, потому что система типов охраняет их разделение. Если же сложение метров с секундами действительно нужно (скажем, по особой формуле), вы делаете это явным преобразованием — и эта явность сигнализирует читателю: «здесь происходит нетривиальный переход, обрати внимание».
Различие между подтипом и производным типом — фундаментальное, и его стоит закрепить таблицей:
| Аспект | subtype (подтип) | type ... is new (производный) |
| Совместимость с базой | совместим, смешивается свободно | несовместим, нужно явное преобразование |
| Зачем нужен | сузить диапазон, оставив родство | развести величины разной природы |
| Пример | subtype Hour is Integer range 0..23; | type Meters is new Integer; |
Производные типы наследуют операции
Тонкость, которая радует: производный тип наследует операции базового. type Meters is new Integer; автоматически получает сложение, вычитание, сравнение — всё, что умеет Integer — но эти операции работают только в пределах Meters. То есть метры можно складывать с метрами (Distance + Distance законно), результат — тоже метры. А вот метры с секундами — нет. Вы получаете полноценную арифметику, замкнутую внутри смыслового типа. Можно ограничить и диапазон при наследовании: type Small_Meters is new Integer range 0 .. 1000; — новый несовместимый тип, да ещё с границами.
Как работает под капотом «одинаковое представление, разные типы»
Возникает законный вопрос: если Meters и Seconds оба представлены как Integer, чем они отличаются в машине? Ответ: в машине — ничем, различие существует только на этапе компиляции. Это так называемая «номинальная типизация»: типы различаются по имени, а не по внутреннему устройству. Во время исполнения метры и секунды — это просто целые той же разрядности, и арифметика над ними столь же быстра. Но компилятор, видя имена типов, не даёт их смешать. Получается, что безопасность достаётся почти бесплатно: вы платите лишь дисциплиной написания преобразований там, где переходите между типами, а в рантайме никаких накладных расходов нет. Это блестящий инженерный компромисс: смысл величин охраняется, скорость не страдает.
Именно поэтому в серьёзных проектах на Ada заводят отдельные типы буквально для всего: идентификаторы пользователей, индексы массивов разных коллекций, физические величины. Когда каждый «вид» числа — свой тип, перепутать их невозможно, и огромный класс ошибок «подставил не то число не туда» исчезает ещё до тестов.
Динамические поддиапазоны и предикаты: ограничения, вычисляемые на лету
До сих пор границы наших типов были константами, известными при компиляции. Но Ada позволяет границам быть динамическими — вычисляемыми во время выполнения. Это раздвигает применимость поддиапазонов далеко за пределы фиксированных лимитов:
procedure Process (Limit : Integer) is
subtype Valid_Index is Integer range 1 .. Limit; -- граница из параметра!
I : Valid_Index;
begin
I := Limit; -- ОК
-- I := Limit + 1; -- Constraint_Error: вне динамической границы
null;
end Process;
Здесь верхняя граница подтипа Valid_Index — это Limit, значение которого станет известно лишь при вызове Process. Подтип формируется заново при каждом входе в процедуру, под актуальное значение параметра. Проверки границ при этом работают так же: попытка выйти за динамическую границу поднимет Constraint_Error. Это исключительно полезно для работы с данными, чей размер известен только в рантайме, — и снова без единого ручного if для проверки границ: ограничение живёт в типе, проверка автоматическая.
Ada 2012 пошла дальше и ввела предикаты подтипов — произвольные логические условия, которым должны удовлетворять значения, не сводящиеся к простому диапазону. Хотите тип «только чётные числа» или «только символы-цифры»? Предикат это позволяет:
subtype Even is Integer
with Dynamic_Predicate => Even mod 2 = 0;
subtype Digit_Char is Character
with Static_Predicate => Digit_Char in '0' .. '9';
Аспект Dynamic_Predicate => Even mod 2 = 0 требует, чтобы любое значение подтипа Even было чётным; присваивание нечётного поднимет Constraint_Error. (Стрелка => в HTML экранируется.) Предикаты превращают подтип из «диапазона» в «произвольное проверяемое множество значений», радикально расширяя выразительность системы типов. Теперь смысловое ограничение почти любой сложности можно закодировать прямо в типе и заставить язык его соблюдать. Это прямое продолжение центральной идеи Ada — знание о допустимых данных живёт в типах, а не в разбросанных проверках, — доведённое до логического предела: не только «число от и до», но и «число, удовлетворяющее любому условию». Вместе с производными типами и динамическими границами предикаты дают инженеру богатейший арсенал, чтобы сделать неправильные данные непредставимыми, а ошибки — невозможными по построению. Заметьте, что в условии предиката встретился оператор in и диапазон '0' .. '9' — типичная для Ada проверка принадлежности значению множеству.
Соберём практический вывод из всего арсенала — поддиапазонов, производных типов, динамических границ, предикатов. Все они служат одной стратегической цели: сделать неправильные данные непредставимыми. Это сдвиг мышления, который отличает зрелого разработчика на Ada. Вместо того чтобы повсюду расставлять проверки «а вдруг здесь отрицательное / вне диапазона / нечётное», вы один раз кодируете ограничение в типе, и дальше компилятор и среда исполнения соблюдают его за вас, во всех точках программы. Проверка перестаёт быть вашей заботой и становится свойством данных. Чем точнее тип описывает реальные допустимые значения, тем больше ошибок становятся попросту невыразимыми: их нельзя написать так, чтобы скомпилировалось и при этом нарушало инвариант. Это и есть инженерная философия «дорого ошибиться» в действии — превентивно закрывать классы ошибок проектированием типов, а не латать их проверками постфактум. Освоив этот образ мысли, вы начнёте проектировать программы, которые трудно использовать неправильно, потому что неправильное использование просто не имеет представления в системе типов.
Частые ошибки и заблуждения
- Путать
subtypeиtype ... is new. Подтип совместим с базой (смешивается свободно), производный тип — несовместим (нужно преобразование). Это разные цели. - Думать, что производный тип «медленнее». Различие чисто компиляционное (номинальное); в рантайме представление и скорость те же, что у базового типа.
- Ожидать, что метры сложатся с секундами «потому что оба целые». Производные типы для того и созданы, чтобы это было ошибкой; смешивать величины разной природы нельзя.
- Забывать, что границы проверяются. Присваивание вне диапазона подтипа/типа даёт
Constraint_Error, а не тихо «обрезается». - Не пользоваться этим инструментом. Заводить отдельные типы под разные величины — идиома Ada, а не излишество; именно она ловит ошибки «не то число не туда».
Итоги
type Имя is range Низ .. Верх;создаёт собственный целочисленный тип с проверяемыми границами.- Подтип (
subtype) сужает диапазон, оставаясь совместимым с базовым типом; так определеныNaturalиPositive. - Производный тип (
type ... is new) создаёт новый несовместимый тип на той же основе — идеально, чтобы развести метры и секунды. - Производные типы наследуют операции базового, но замыкают их внутри себя: метры складываются с метрами, но не с секундами.
- Различие типов номинальное (по имени, на этапе компиляции); в рантайме представление и скорость не меняются — безопасность почти бесплатна.