Массивы: ограниченные, неограниченные и атрибуты
Массивы в Ada: ограниченные и неограниченные, индексация любым дискретным типом, атрибуты границ и длины. Почему выход за границу здесь невозможен незаметно и как один тип массива работает с любым размером.
Массив в Ada — упорядоченный набор элементов одного типа, индексируемый дискретным типом; границы могут быть зафиксированы в типе (ограниченный массив) или задаваться при создании объекта (неограниченный массив).
Зачем массивам в Ada отдельный серьёзный разговор
Массив есть в любом языке, но в Ada он спроектирован с той же одержимостью безопасностью, что и всё остальное. Здесь массив помнит свои границы, проверяет каждое обращение по индексу и может индексироваться не только числами. Выход за границу массива — классическая дыра, через которую в других языках утекают данные и происходят атаки переполнения буфера, — в Ada превращается в проверяемую ошибку Constraint_Error, а не в тихое чтение чужой памяти. Понять устройство массивов Ada — значит понять, как язык делает безопасной самую частую структуру данных.
Ограниченные массивы: границы в типе
Простейший случай — массив с фиксированными границами, заданными прямо в типе:
type Week_Temps is array (1 .. 7) of Float;
T : Week_Temps;
begin
T (1) := 20.5; -- доступ к элементу по индексу в круглых скобках
T (7) := 18.0;
Объявление array (1 .. 7) of Float создаёт тип массива из семи вещественных, индексируемых от 1 до 7. Обращение к элементу — через круглые скобки: T (1). Это отличает Ada от языков с квадратными скобками и неслучайно: в Ada индексация массива синтаксически совпадает с вызовом функции, что отражает глубокую идею — массив можно мыслить как отображение индекса в значение. Обращение T (8) или T (0) выйдет за границы и поднимет Constraint_Error — никакого молчаливого чтения соседней памяти.
Индексировать можно любым дискретным типом
Глубокая особенность: индексом массива в Ada может быть любой дискретный тип, а не только целые от нуля. Это исключительно выразительно. Индексируем массив днями недели:
type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
type Schedule is array (Day) of Boolean; -- индекс — перечисление!
Busy : Schedule := (Mon .. Fri => True, Sat | Sun => False);
begin
if Busy (Wed) then
Put_Line ("В среду занято");
end if;
Здесь массив Schedule индексируется днями недели: Busy (Wed) читается осмысленно — «занятость в среду». Никаких «магических» числовых индексов, где надо помнить, что 2 — это среда. Это снова про читаемость и про устранение ошибок: индекс-перечисление невозможно перепутать с произвольным числом. Можно индексировать и символами (array (Character) of ...) — удобно для таблиц частот букв.
Атрибуты массива: границы, диапазон, длина
К массивам применимы знакомые атрибуты, и здесь они особенно полезны:
T'First -- первый индекс (для Week_Temps: 1)
T'Last -- последний индекс (7)
T'Range -- весь диапазон индексов (1 .. 7)
T'Length -- число элементов (7)
Главная идиома — перебор массива через 'Range, не вписывая границы руками:
Total : Float := 0.0;
for I in T'Range loop -- проходим ровно по индексам T
Total := Total + T (I);
end loop;
Этот цикл корректен при любых границах массива: измените тип на array (1 .. 30), и цикл сам подстроится, ведь он опирается на T'Range, а не на число 7. Так комбинируются две идеи Ada — атрибуты и циклы — давая код, устойчивый к изменениям и защищённый от выхода за границы. Писать for I in 1 .. 7 вместо for I in T'Range считается дурным тоном именно потому, что хрупко.
Неограниченные массивы: один тип на любой размер
Теперь — мощнейшая концепция. Часто заранее неизвестно, какого размера будет массив: число прочитанных строк, длина введённого текста. Заводить отдельный тип под каждый размер абсурдно. Ada решает это неограниченными массивами (unconstrained arrays), где границы в типе оставлены открытыми, а задаются при создании конкретного объекта:
type Int_Vector is array (Positive range <>) of Integer;
-- ^^^^ "box": границы будут позже
A : Int_Vector (1 .. 5); -- этот объект имеет 5 элементов
B : Int_Vector (1 .. 100); -- а этот — 100, но тип у них ОДИН
Загадочный символ <> (в коде это «box» — две угловые скобки, в HTML экранируется как <>) означает «границы не зафиксированы в типе, они будут указаны при объявлении объекта». Конструкция Positive range <> читается как «индекс типа Positive, диапазон уточняется позже». Теперь A и B — один и тот же тип Int_Vector, но разной длины. Это критически важно: подпрограмма может принимать Int_Vector любого размера одним параметром, не зная заранее длину:
function Sum (V : Int_Vector) return Integer is
Result : Integer := 0;
begin
for I in V'Range loop -- работает для массива ЛЮБОЙ длины
Result := Result + V (I);
end loop;
return Result;
end Sum;
Функция Sum просуммирует и пятиэлементный A, и стоэлементный B, потому что внутри она спрашивает V'Range и V'Length у переданного массива, а не полагается на фиксированный размер. Сам предопределённый тип String в Ada устроен именно так: это array (Positive range <>) of Character — неограниченный массив символов, поэтому строки бывают любой длины, оставаясь одним типом.
Операции над массивами целиком
Массивы в Ada можно присваивать и сравнивать целиком, как единое значение:
A : Week_Temps := (others => 0.0); -- весь массив инициализирован нулями
B : Week_Temps;
B := A; -- копирование всего массива одним присваиванием
if A = B then ... -- сравнение всех элементов сразу
Запись (others => 0.0) — это агрегат, заполняющий все элементы значением 0.0 (об агрегатах подробнее в следующем уроке). Присваивание B := A копирует весь массив, а A = B сравнивает поэлементно. Для одномерных массивов работает и конкатенация оператором &, как у строк. Эти операции «над массивом целиком» лаконичны и безопасны: длины проверяются, выход за границы исключён.
Как работает под капотом проверка границ
Откуда берётся защита от выхода за границу? Каждый объект-массив в Ada знает свои границы — они доступны через 'First и 'Last. При обращении A (I) среда исполнения сверяет, что I лежит в A'First .. A'Last; если нет — поднимается Constraint_Error ровно в точке нарушения, а не где-то потом. Это та самая граница между «ошибка обнаружена честно» и «программа читает чужую память». Знаменитые уязвимости переполнения буфера, на которых строятся атаки в C-подобных языках, в Ada в принципе не работают так же: язык не даст выйти за границу незаметно. Компилятор при этом умён: если он может статически доказать, что индекс заведомо в границах (например, в цикле for I in A'Range), он убирает проверку как ненужную — производительность не страдает там, где безопасность доказана. Получается оптимальный баланс: проверки есть там, где нужны, и нет там, где доказано, что выход невозможен. Это инженерный идеал, к которому Ada стремится во всём — безопасность по умолчанию, без лишней платы.
Многомерные массивы и срезы
Массивы в Ada не ограничены одним измерением — язык поддерживает настоящие многомерные массивы, индексируемые несколькими индексами сразу. Это удобно для матриц, таблиц, сеток:
type Matrix is array (1 .. 3, 1 .. 3) of Float;
M : Matrix := (others => (others => 0.0)); -- нулевая матрица 3x3
begin
M (1, 1) := 1.0; -- доступ по ДВУМ индексам
M (2, 3) := 5.0;
Тип array (1 .. 3, 1 .. 3) — это двумерный массив; обращение к элементу требует двух индексов в одних скобках: M (2, 3). Заметьте вложенный агрегат (others => (others => 0.0)): внешний others покрывает строки, внутренний — элементы в строке, и вся матрица заполняется нулями одним выражением. (Стрелки => в HTML экранируются.) У многомерного массива атрибуты принимают номер измерения: M'First(1) и M'Last(1) — границы первого измерения, M'Length(2) — размер второго. Перебор матрицы — это вложенные циклы по 'Range каждого измерения, остающиеся корректными при любом размере.
Важно, что многомерный массив в Ada — это не «массив массивов», а единая сущность с несколькими индексами, хранящаяся непрерывно. Это отличает его от языков, где двумерность эмулируется массивом ссылок на строки. Непрерывное хранение предсказуемо по памяти и эффективно по доступу — что снова ценят встраиваемые системы. (Если же нужен именно «массив массивов» с разной длиной строк, его строят явно, как массив записей или неограниченных структур.)
Вернёмся к одномерным массивам ради ещё одной важной возможности — срезов (slices). Срез — это обращение к непрерывному участку массива по диапазону индексов, и он сам является массивом того же типа:
type Buffer is array (1 .. 10) of Integer;
B : Buffer := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Middle : Buffer (3 .. 6) := B (3 .. 6); -- срез: элементы с 3 по 6
begin
B (1 .. 3) := B (8 .. 10); -- скопировать хвост в начало, целым срезом
Запись B (3 .. 6) даёт срез из четырёх элементов как самостоятельное массивное значение. Срезы можно читать, присваивать и передавать в подпрограммы. Присваивание B (1 .. 3) := B (8 .. 10) копирует целый участок одной операцией, с проверкой совпадения длин. Срезы особенно естественны для строк (которые, как мы знаем, тоже массивы): выделить подстроку — это взять срез. Многомерность и срезы показывают, что массив в Ada — богатая, гибкая структура, в которой операции над участками и измерениями выражаются ясно и безопасно, с теми же гарантиями проверки границ, что и доступ к одиночному элементу.
Частые ошибки и заблуждения
- Обращаться к элементу квадратными скобками. В Ada индексация — круглые скобки:
A (I), а неA[I]; синтаксис совпадает с вызовом функции. - Вписывать границы цикла вручную. Используйте
for I in A'Range: цикл подстроится под размер и не выйдет за границы;for I in 1 .. 7хрупко. - Считать выход за границу «тихим». Он поднимает
Constraint_Errorв точке нарушения — переполнения буфера, как в C, здесь не происходит. - Заводить отдельный тип под каждый размер. Используйте неограниченный массив (
range <>): один тип работает с объектами любой длины, и подпрограммы принимают любой размер. - Забывать, что массивы — значения целиком. Их можно присваивать, сравнивать и конкатенировать целиком; не нужно копировать поэлементно вручную.
Итоги
- Массив — упорядоченный набор однотипных элементов; обращение по индексу через круглые скобки
A (I), индексом служит любой дискретный тип, включая перечисления. - Ограниченный массив фиксирует границы в типе; атрибуты
'First/'Last/'Range/'Lengthдают границы, диапазон и размер. - Неограниченный массив (
array (Positive range <>) of ...) оставляет границы открытыми: один тип работает с объектами любой длины, как предопределённыйString. - Массивы — значения целиком: их можно присваивать, сравнивать и (одномерные) конкатенировать оператором
&. - Каждый массив знает границы; выход за них даёт
Constraint_Error, а доказуемо безопасные обращения компилятор оптимизирует — безопасность без лишней платы.