Агрегаты: составное значение одним выражением
Агрегаты — способ задать составное значение целиком одним выражением: позиционно, по именам индексов и полей, с others и диапазонами. Почему агрегаты безопаснее поэлементного заполнения.
Агрегат — это литерал составного значения (массива или записи), записанный в круглых скобках, который задаёт все компоненты сразу; компилятор проверяет полноту покрытия.
Зачем нужны агрегаты
Заполнять массив или запись по одному элементу — это и многословно, и опасно: легко забыть какой-нибудь индекс или поле, оставив его с мусором. Ada предлагает изящную альтернативу — агрегат, выражение, задающее всё составное значение целиком. Это не просто синтаксическое удобство: агрегат — единое выражение, и компилятор проверяет, что в нём покрыты все компоненты. Забыли элемент — ошибка компиляции, а не молчаливо неинициализированное значение. Агрегаты воплощают принцип Ada «лучше задать всё явно, чем оставить на волю случая». В этом уроке мы сосредоточимся на агрегатах массивов; для записей они работают аналогично (увидим в уроке про записи).
Позиционные агрегаты
Простейшая форма — перечислить значения по порядку, как они идут по индексам:
type Triple is array (1 .. 3) of Integer;
A : Triple := (10, 20, 30); -- A(1)=10, A(2)=20, A(3)=30
Значения в круглых скобках через запятую присваиваются элементам по позициям: первое — первому индексу, и так далее. Важная строгость: в позиционном агрегате число значений должно точно совпадать с числом элементов. Напишете два значения для трёхэлементного массива — ошибка. Это и есть проверка полноты: компилятор не даст создать «недозаполненный» массив.
Именованные агрегаты: по индексам
Более выразительная форма — указать, какому индексу какое значение, через стрелку =>:
A : Triple := (1 => 10, 2 => 20, 3 => 30); -- то же самое, но явно
Здесь 1 => 10 читается «индексу 1 — значение 10». (Стрелка => в HTML экранируется.) Именованная форма самодокументирующаяся и не зависит от порядка: можно написать (3 => 30, 1 => 10, 2 => 20), и значения встанут по своим индексам. Особенно она полезна, когда индекс — перечисление:
type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
type Hours is array (Day) of Natural;
Work : Hours := (Mon => 8, Tue => 8, Wed => 8,
Thu => 8, Fri => 6, Sat => 0, Sun => 0);
Этот агрегат читается как таблица: понедельник — 8 часов, пятница — 6, выходные — 0. Никаких загадочных позиций. Здесь компилятор тоже проверит, что покрыты все семь дней; забудете воскресенье — ошибка.
Диапазоны и объединение в агрегатах
В именованных агрегатах можно задавать несколько индексов сразу — диапазоном .. или объединением |:
type Bitmap is array (1 .. 10) of Boolean;
B : Bitmap := (1 .. 5 => True, 6 .. 10 => False);
C : Bitmap := (1 | 3 | 5 | 7 | 9 => True, others => False);
В B элементы с 1 по 5 истинны, с 6 по 10 — ложны. В C нечётные индексы истинны, а others (все прочие, то есть чётные) — ложны. Диапазон 1 .. 5 => True присваивает значение целой группе элементов одним махом — лаконично и наглядно.
Спасительный others
Ключевое слово others в агрегате означает «все оставшиеся компоненты». Это и удобно, и важно для полноты:
Zeros : Triple := (others => 0); -- все элементы = 0
Mixed : array (1 .. 100) of Integer :=
(1 => 50, others => 0); -- первый = 50, остальные 99 = 0
(others => 0) — самый частый способ инициализировать весь массив одним значением, не перечисляя каждый элемент. Это куда лучше, чем цикл присваивания нулей, и читается мгновенно. others также гарантирует полноту: что бы вы ни перечислили явно, others закроет остаток, и неинициализированных элементов не останется. При этом others должен идти последним в агрегате. Запомните идиому (others => начальное_значение) — она встречается в коде Ada постоянно.
Агрегаты как полноценные значения
Агрегат — это выражение, значение составного типа, а не только инструмент инициализации. Его можно передать в подпрограмму, вернуть из функции, присвоить существующей переменной:
A : Triple;
-- ...
A := (100, 200, 300); -- присваивание агрегата готовой переменной
Print_Triple ((1, 2, 3)); -- передача агрегата как аргумента
Это делает код выразительным: вы создаёте составное значение «на лету» там, где оно нужно, без временных переменных. Например, функция может вернуть массив-агрегат напрямую. Современные ревизии Ada сделали агрегаты ещё гибче (в том числе для контейнеров), но базовая идея неизменна: целое составное значение записывается одним выражением, и оно полное.
Как работает под капотом проверка полноты агрегата
Почему агрегаты безопаснее поэлементного заполнения? Потому что для агрегата компилятор статически проверяет покрытие всех компонентов. Зная из типа точные границы массива (или точный набор полей записи), он сверяет, что агрегат задаёт каждый из них — явно, через диапазон, через | или через others. Если хоть один компонент остался без значения и нет others — это ошибка компиляции «не все компоненты заданы». Сравните с ручным заполнением в цикле или присваиваниями: там забытый элемент просто останется с неопределённым значением, и баг проявится позже. Агрегат превращает «забыл инициализировать» из тихой проблемы рантайма в явную ошибку на столе. Это та же философия, что у обязательного покрытия в case: точные типы плюс требование полноты дают компилятору доказать, что ничего не пропущено. Дополнительный бонус — производительность: компилятор видит всё составное значение сразу и может построить его эффективно, иногда прямо как константу в памяти, без пошагового заполнения в рантайме. Безопасность и скорость снова идут рука об руку.
Маленькая программа, печатающая массив, заполненный агрегатом:
with Ada.Text_IO; use Ada.Text_IO;
procedure Aggregate_Demo is
type Scores is array (1 .. 4) of Integer;
S : Scores := (1 => 90, 2 => 75, others => 0);
begin
for I in S'Range loop
Put_Line ("Элемент" & Integer'Image (I) & " =" & Integer'Image (S (I)));
end loop;
end Aggregate_Demo;
Вывод:
Элемент 1 = 90 Элемент 2 = 75 Элемент 3 = 0 Элемент 4 = 0
Агрегаты записей и обновление дельтой
Мы разбирали агрегаты на примере массивов, но они столь же фундаментальны для записей — и там их полнота проверяется ещё нагляднее, ведь у записи фиксированный, поимённо известный набор полей. Агрегат записи задаёт все поля сразу, позиционно или по именам:
type Point is record
X, Y, Z : Float;
end record;
P1 : Point := (1.0, 2.0, 3.0); -- позиционно
P2 : Point := (X => 0.0, Y => 0.0, Z => 1.0); -- по именам полей
Именованная форма (X => 0.0, Y => 0.0, Z => 1.0) самодокументирующаяся, а компилятор проверит, что заданы все поля. (Стрелки => в HTML экранируются.) Если у записи много полей, а вы хотите задать остальные одинаково, помогает others: (X => 5.0, others => 0.0). Так же, как с массивами, забытое поле без значения по умолчанию — ошибка компиляции, и это гарантирует, что объект записи никогда не родится частично неинициализированным.
Особенно элегантна введённая в Ada 2012 дельта-агрегация — создание нового составного значения как «копии существующего с изменением нескольких компонентов». Часто нужно получить значение, отличающееся от имеющегося лишь парой полей; писать полный агрегат, перечисляя всё неизменное, утомительно и хрупко. Дельта-агрегат решает это:
P3 : Point := (P1 with delta Z => 99.0);
-- P3 равен P1, но с Z = 99.0; X и Y взяты из P1 без перечисления
Конструкция (P1 with delta Z => 99.0) читается буквально: «как P1, но с изменённым Z». Все поля, кроме явно указанных, копируются из исходного значения. Это работает и для массивов: (A with delta 3 => 0) — копия массива с обнулённым третьим элементом. Дельта-агрегаты особенно ценны для неизменяемых данных и функционального стиля: вместо мутации объекта вы порождаете новое значение с нужным отличием, ясно и кратко, не теряя проверки полноты типа.
Глубокая мысль, объединяющая всё про агрегаты: Ada последовательно предпочитает работу с составными значениями целиком, а не по кусочкам. Создать массив или запись — один агрегат; скопировать — одно присваивание; изменить несколько полей — один дельта-агрегат; сравнить — один оператор =. Этот «целостный» стиль не только лаконичнее, но и безопаснее поэлементных манипуляций: на каждом шаге компилятор видит всё значение и проверяет его полноту и согласованность типов. Меньше ручных шагов — меньше мест, где можно ошибиться и оставить структуру в недоделанном состоянии. Для надёжных систем это ровно тот стиль, который снижает число дефектов.
Стоит отметить и связь агрегатов с контрактами и безопасностью, замыкающую тему. Поскольку агрегат задаёт значение целиком и компилятор проверяет его полноту и типы, агрегат — это естественная точка, где данные впервые обретают корректную форму. Объект, созданный полным агрегатом, гарантированно согласован: все поля заданы, все типы совпали. Это куда надёжнее, чем «создать пустым и постепенно дозаполнить», где между шагами объект существует в недоделанном, потенциально невалидном состоянии. В сочетании с инвариантами типов (которые мы затронем дальше) агрегаты позволяют строить значения, что сразу удовлетворяют всем инвариантам, без окна несогласованности. Поэтому идиоматичный код Ada тяготеет к созданию составных данных одним выражением и к их обновлению дельта-агрегатами, а не к пошаговой мутации. Этот стиль — ещё одна грань общей стратегии языка: минимизировать число состояний, в которых данные могут быть неправильными, в идеале сведя их к нулю. Где данные всегда либо отсутствуют, либо полностью корректны, там целый пласт ошибок «использовал недозаполненную структуру» исчезает сам собой.
Частые ошибки и заблуждения
- Задавать неполный позиционный агрегат. Число значений должно точно совпасть с числом элементов; иначе ошибка. Для «остатка» используйте
others. - Ставить
othersне последним.othersобязан идти в конце агрегата, после всех явных компонентов. - Смешивать позиционную и именованную формы как попало. Можно начать с позиционных и закончить именованными/
others, но не вперемешку произвольно; чаще выбирают одну форму ради ясности. - Заполнять массив циклом, когда хватит агрегата.
(others => 0)и диапазоны короче, читаемее и безопаснее ручного цикла инициализации. - Считать агрегат «только для объявления». Это полноценное выражение: его можно присваивать, передавать в подпрограммы и возвращать из функций.
Итоги
- Агрегат — литерал составного значения в круглых скобках, задающий все компоненты сразу; компилятор проверяет полноту покрытия.
- Формы: позиционная (по порядку), именованная (
индекс => значение), с диапазонами (1 .. 5 => ...) и объединением (a | b => ...). - Идиома
(others => значение)заполняет оставшиеся (или все) компоненты и гарантирует, что неинициализированных не останется;othersидёт последним. - Агрегат — полноценное выражение: его можно присваивать, передавать и возвращать, создавая составные значения «на лету».
- Проверка полноты статическая: забытый компонент — ошибка компиляции, а не тихий баг; вдобавок агрегаты эффективны при построении.