Массивы: ограниченные, неограниченные и атрибуты

Массивы в 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, а доказуемо безопасные обращения компилятор оптимизирует — безопасность без лишней платы.
Проверьте себя
1. Что произойдёт при обращении к элементу массива за его границей?
AПрочитается соседняя память
BПоднимется Constraint_Error в точке нарушения
CВернётся 0
DПрограмма продолжит молча
2. Что даёт объявление array (Positive range <>) of Integer?
AОшибку
BНеограниченный массив: один тип работает с объектами любой длины
CМассив ровно на 0 элементов
DДвумерный массив
3. Каким типом можно индексировать массив в Ada?
AТолько целыми от нуля
BЛюбым дискретным типом, включая перечисления и символы
CТолько Float
DТолько String