Строки: String и Unbounded_String

Строки в Ada: фиксированный String как массив символов, его сила и неудобства, и Unbounded_String для текста переменной длины. Почему у Ada не одна «универсальная» строка, а семейство — и как выбирать.

String в Ada — это неограниченный массив символов фиксированной (после создания) длины; Unbounded_String — отдельный тип для строк, свободно меняющих длину во время работы.

String — это массив символов

Мы уже знаем секрет: в Ada строка String — не магический встроенный тип, а просто массив символов. Точнее, в стандарте она определена как type String is array (Positive range <>) of Character; — неограниченный массив, индексируемый положительными числами. Из этого вытекает всё поведение строк, которое поначалу удивляет, но становится логичным, когда помнишь про массивы:

S : String (1 .. 5) := "Ариана"(1 .. 5);   -- строка ровно из 5 символов
Greeting : constant String := "Привет";    -- длина выводится из литерала (6)

C : Character := Greeting (1);              -- доступ к символу как к элементу массива
Len : Natural := Greeting'Length;           -- длина через атрибут массива

Поскольку String — массив, к нему применимо всё из урока про массивы: индексация круглыми скобками Greeting (1), атрибуты 'First/'Last/'Range/'Length, проверка границ. Строковый литерал "Привет" — это агрегат-значение типа String, и его длина определяет границы. Объявляя S : String (1 .. 5), вы фиксируете длину пятью символами — ровно как у любого ограниченного массива.

Главное ограничение фиксированной строки

И тут же — главная особенность String: его длина фиксируется при создании и не меняется. Это прямое следствие того, что массив имеет фиксированные границы. Нельзя «дописать» символ в конец String, удлинив его, — длина задана и неизменна. Если переменная объявлена как String (1 .. 10), она всегда ровно десятисимвольная. Отсюда знакомая по уроку про ввод-вывод морока с Get_Line (Name, Last): вы заводите массив с запасом, читаете в него и отдельно храните, сколько символов реально заняты (Last), потому что сам массив не может «сжаться» до фактической длины. Для многих задач фиксированная строка прекрасна — она предсказуема, не требует динамической памяти, идеальна для встраиваемых систем. Но для текста, который растёт и меняется, она неудобна.

Срезы и конкатенация строк

Раз строка — массив, работают срезы (slices) — обращение к части массива по диапазону:

Full : constant String := "Ада Лавлейс";
First_Name : String := Full (1 .. 3);     -- срез: "Ада"

-- конкатенация оператором &
Hello : String := "Привет, " & "мир";     -- "Привет, мир"
With_Excl : String := Hello & "!";        -- "Привет, мир!"

Срез Full (1 .. 3) даёт подстроку из символов с 1 по 3 — это полноценное строковое значение. Конкатенация оператором & (в HTML — &amp;) склеивает строки, давая новую строку нужной длины. Заметьте: результат конкатенации — новая строка со своей длиной, а не изменение исходной. Это согласуется с природой фиксированных массивов: вы не растёте на месте, а создаёте новое значение.

Пакет Ada.Strings.Fixed

Операции над фиксированными строками (поиск подстроки, замена, дополнение пробелами, удаление) собраны в пакете Ada.Strings.Fixed. Например, найти позицию подстроки:

with Ada.Strings.Fixed;  use Ada.Strings.Fixed;
-- ...
Pos : Natural := Index ("программирование", "грамм");   -- вернёт начальную позицию

Функция Index возвращает позицию первого вхождения подстроки (или 0, если не найдена). В этом пакете много полезного для разбора и форматирования текста фиксированной длины, и он не требует динамической памяти.

Unbounded_String: строки, которые растут

Когда фиксированная длина мешает, на сцену выходит Unbounded_String из пакета Ada.Strings.Unbounded. Это отдельный тип для строк произвольной, меняющейся длины — близкий по духу к строкам из языков вроде Python или Java, где строка просто «есть» нужного размера:

with Ada.Strings.Unbounded;  use Ada.Strings.Unbounded;
with Ada.Text_IO;            use Ada.Text_IO;

procedure Unbounded_Demo is
   U : Unbounded_String := To_Unbounded_String ("Привет");
begin
   U := U & ", мир";                  -- свободно удлиняем
   Append (U, "!");                   -- добавляем ещё
   Put_Line (To_String (U));          -- печатаем как обычную строку
   Put_Line ("Длина:" & Integer'Image (Length (U)));
end Unbounded_Demo;

Вывод:

Привет, мир!
Длина: 13

Ключевые операции: To_Unbounded_String делает Unbounded_String из обычного String, To_String — обратно; Append или оператор & удлиняют строку; Length даёт текущую длину. Здесь строка свободно растёт от «Привет» до «Привет, мир!» — никакого ручного учёта границ. Обратите внимание на необходимость преобразований To_String / To_Unbounded_String на границе двух мiров: это та самая явность Ada — переход между фиксированным и неограниченным представлением виден в коде.

Почему не одна «универсальная» строка

Программиста из языков с единственным строковым типом удивляет: зачем Ada целое семейство — String, Bounded_String (с ограниченным максимумом), Unbounded_String? Ответ — в инженерной честности. Разные задачи требуют разных компромиссов между гибкостью и предсказуемостью памяти:

ТипДлинаПамятьКогда применять
Stringфиксирована при созданиистатическая, без кучиизвестная длина, встраиваемые системы
Bounded_Stringдо заданного максимумастатическая, с лимитоместь верхний предел, куча нежелательна
Unbounded_Stringлюбая, меняетсядинамическая (куча)текст растёт непредсказуемо

В системе реального времени, где динамическое выделение памяти запрещено (оно непредсказуемо по времени), Unbounded_String использовать нельзя — берут String или Bounded_String. В обычном приложении, где удобство важнее, Unbounded_String избавляет от возни с границами. Ada не навязывает один компромисс, а даёт выбрать осознанно под требования системы. Это та же философия, что в выборе семантики переполнения через тип: язык предоставляет инструменты, а решение — за инженером, который знает контекст.

Как работает под капотом и зачем явные преобразования

Под капотом String — это просто непрерывный кусок символов фиксированного размера, без накладных расходов, как массив. Unbounded_String же внутри управляет динамически выделенным буфером, который автоматически растёт при необходимости (и память освобождается, когда строка выходит из области видимости — Ada делает это за вас, без ручного управления). Платой за гибкость Unbounded_String является обращение к куче и чуть большие накладные расходы — поэтому в критичных по времени местах его избегают. Явные преобразования To_String / To_Unbounded_String на стыке — не бюрократия, а отражение того, что это разные типы с разной семантикой памяти, и переход между ними — реальная операция, которую полезно видеть. В этом весь подход Ada: ничего не происходит неявно, у каждого выбора видны последствия, и инженер всегда контролирует, где статическая память, а где динамическая. Для систем, которым доверяют жизни, такой контроль над памятью бесценен.

Строки и кодировки: Wide_String и многоязычный текст

Есть тема, которую нельзя обойти, говоря о строках в современном мире, — кодировки символов и многоязычный текст. Тип Character в Ada — это, по сути, один байт (Latin-1, 256 значений), а String — массив таких байтов. Для английского и многих европейских языков этого хватает, но для полноценной поддержки всех письменностей мира (включая кириллицу за пределами Latin-1, китайский, эмодзи) одного байта мало. Поэтому Ada предоставляет семейство символьных и строковых типов под разную ширину символа:

СимволСтрокаШирина
CharacterString8 бит (Latin-1)
Wide_CharacterWide_String16 бит (BMP Unicode)
Wide_Wide_CharacterWide_Wide_String32 бита (полный Unicode)

Типы Wide_String и Wide_Wide_String устроены точно так же, как String — это неограниченные массивы соответствующих символов, — но их элементы шире и вмещают весь диапазон Unicode. Для них есть свои варианты ввода-вывода (Ada.Wide_Text_IO) и свои строковые пакеты. Принцип тот же, что и с обычными строками: единая модель «строка как массив символов», просто параметризованная шириной символа. Это снова проявление регулярности Ada — не отдельный «класс юникод-строки» с особым API, а та же конструкция с более ёмким элементом.

Для практической работы с UTF-8 (доминирующей кодировкой текста сегодня) в стандарте есть пакет Ada.Strings.UTF_Encoding с функциями перекодирования между представлениями. Типичный подход в современной программе: хранить и обрабатывать текст в Wide_Wide_String (где один элемент — один кодовый знак Unicode), а на границах ввода-вывода преобразовывать в UTF-8 и обратно. Так логика работает с «настоящими» символами, не путаясь в байтах, а внешний обмен идёт в компактном UTF-8.

Эта градация ширины символа — ещё одно проявление сквозного принципа раздела: Ada не навязывает один компромисс, а даёт осознанный выбор под требования. Встраиваемой системе, работающей только с ASCII-командами, незачем платить за 32-битные символы — она берёт String. Многоязычному приложению нужен полный Unicode — оно берёт Wide_Wide_String. Как и в выборе между String и Unbounded_String, как и в семантике переполнения через тип, язык предоставляет инструменты и оставляет решение инженеру, который один знает контекст. Текст — казалось бы, простая вещь — на деле полон компромиссов между памятью, охватом и скоростью, и зрелость Ada в том, что она делает эти компромиссы видимыми и управляемыми, а не прячет их за единственным «универсальным» типом, который для одних задач избыточен, а для других недостаточен.

Частые ошибки и заблуждения

  • Пытаться удлинить String. Его длина фиксирована при создании; «дописать» нельзя — конкатенация создаёт новую строку. Для роста нужен Unbounded_String.
  • Забывать про Last при чтении в String. Фиксированный массив не сжимается до фактической длины; печатайте срез (1 .. Last).
  • Смешивать String и Unbounded_String напрямую. Это разные типы; преобразуйте через To_String / To_Unbounded_String.
  • Использовать Unbounded_String в системе реального времени без оглядки. Он работает с кучей (динамическая память); там, где она запрещена, берите String или Bounded_String.
  • Искать «одну правильную строку». Ada намеренно даёт семейство типов под разные компромиссы память/гибкость; выбор зависит от требований системы.

Итоги

  • String — это неограниченный массив символов; к нему применимы индексация, срезы, атрибуты и конкатенация &, но его длина фиксируется при создании.
  • Операции над фиксированными строками (поиск, замена) собраны в Ada.Strings.Fixed и не требуют динамической памяти.
  • Unbounded_String из Ada.Strings.Unbounded хранит текст переменной длины; Append/& удлиняют, Length даёт размер, преобразования — To_String/To_Unbounded_String.
  • Семейство строк (String, Bounded_String, Unbounded_String) даёт выбор между предсказуемостью статической памяти и гибкостью динамической.
  • Явные преобразования и отсутствие «универсальной» строки — это контроль над памятью: видно, где статика, где куча, что критично для надёжных систем.
Проверьте себя
1. Почему длину обычного String нельзя увеличить после создания?
AЭто можно
BПотому что String — массив символов с фиксированными границами; конкатенация создаёт новую строку
CString запрещает буквы
DИз-за бага компилятора
2. Когда стоит избегать Unbounded_String?
AНикогда
BВ системах реального времени без динамической памяти — он работает с кучей
CВ любых программах
DТолько в тестах
3. Зачем Ada даёт целое семейство строк, а не одну универсальную?
AИсторическая случайность
BЧтобы инженер осознанно выбирал компромисс между предсказуемостью статической памяти и гибкостью динамической
CЧтобы усложнить язык
DИз-за разных алфавитов