Строки: 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 — &) склеивает строки, давая новую строку нужной длины. Заметьте: результат конкатенации — новая строка со своей длиной, а не изменение исходной. Это согласуется с природой фиксированных массивов: вы не растёте на месте, а создаёте новое значение.
Пакет 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 предоставляет семейство символьных и строковых типов под разную ширину символа:
| Символ | Строка | Ширина |
Character | String | 8 бит (Latin-1) |
Wide_Character | Wide_String | 16 бит (BMP Unicode) |
Wide_Wide_Character | Wide_Wide_String | 32 бита (полный 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) даёт выбор между предсказуемостью статической памяти и гибкостью динамической. - Явные преобразования и отсутствие «универсальной» строки — это контроль над памятью: видно, где статика, где куча, что критично для надёжных систем.