Базовый ввод-вывод: семейство Text_IO
Как программа на Ada разговаривает с миром: печать текста, перевод строк, вывод чисел и базовое чтение ввода через семейство пакетов Text_IO. Почему ввод-вывод здесь типизирован, а не «всё подряд».
Ada.Text_IO — стандартный пакет текстового ввода-вывода: печатает и читает строки и символы; для чисел используют его специализированные «родственники».
Почему ввод-вывод устроен пакетами
В Ada нет встроенного оператора печати — вывод предоставляется библиотекой, пакетом Ada.Text_IO. На первый взгляд это многословнее, чем волшебное ключевое слово печати в других языках. Но за этим стоит принцип: язык остаётся минимальным и чистым, а возможности добавляются библиотеками с чёткими типизированными интерфейсами. И вот ключевая особенность — ввод-вывод в Ada типизирован. Нельзя одной универсальной командой напечатать «что угодно» с автоматическим угадыванием формата. Для строк один набор операций, для целых — другой, для вещественных — третий. Это кажется строгостью, но обеспечивает контроль над форматом и исключает сюрпризы.
Печать строк: Put и Put_Line
Две главные операции вывода текста:
Put— печатает текст без перевода строки в конце.Put_Line— печатает текст и переводит строку.
with Ada.Text_IO;
use Ada.Text_IO;
procedure Demo_Output is
begin
Put ("Раз ");
Put ("два ");
Put_Line ("три"); -- здесь произойдёт перевод строки
Put_Line ("Новая строка");
end Demo_Output;
Вывод:
Раз два три Новая строка
Видно, что три первых Put склеились в одну строку, а Put_Line завершил её. Отдельно перевести строку можно процедурой New_Line (без аргумента — один перевод, с числом — несколько):
Put ("Заголовок");
New_Line (2); -- две пустые строки после
Put ("Тело");
Печать одиночного символа — тоже Put, но с аргументом-символом: Put ('X');. Компилятор сам выбирает нужную версию Put по типу аргумента (строка это или символ) — это работает механизм перегрузки, к которому мы вернёмся.
Вывод чисел: специализированные пакеты
А как напечатать число? Передать целое в строковый Put нельзя — типы не совпадут. Для чисел есть отдельные пакеты:
Ada.Integer_Text_IO— для целых;Ada.Float_Text_IO— для вещественных.
Они тоже предоставляют процедуру Put, но принимающую число. Подключив оба пакета, можно печатать и текст, и числа:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Print_Number is
Age : Integer := 25;
begin
Put ("Возраст: "); -- строковый Put из Ada.Text_IO
Put (Age); -- числовой Put из Ada.Integer_Text_IO
New_Line;
end Print_Number;
Вывод:
Возраст: 25
Заметили лишние пробелы перед числом? По умолчанию целое печатается в поле фиксированной ширины, выровненное вправо. Это управляется параметром Width. Чтобы убрать выравнивание, задают ширину 0:
Put (Age, Width => 0); -- напечатает ровно "25"
Конструкция Width => 0 — это именованный аргумент: мы явно называем параметр Width и стрелкой => задаём ему значение. (В HTML стрелка экранируется как =>.) Именованные аргументы — характерная и очень читаемая черта Ada: из вызова сразу понятно, что именно настраивается. Для вещественных чисел у Put есть параметры Fore (цифры до точки), Aft (после точки) и Exp (экспонента), что даёт точный контроль формата:
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
-- ...
Put (3.14159, Fore => 1, Aft => 2, Exp => 0); -- "3.14"
Конкатенация и преобразование в строку
Часто хочется собрать сообщение из кусков. Строки склеиваются оператором & (в HTML — &, в коде один символ):
Name : constant String := "Ариана";
Put_Line ("Запуск: " & Name & " готов");
Но склеить строку с числом напрямую нельзя — типы разные. Чтобы превратить число в строку, используют атрибут 'Image, дающий текстовое представление значения:
Count : constant Integer := 42;
Put_Line ("Всего: " & Integer'Image (Count));
Вывод:
Всего: 42
Запись Integer'Image (Count) читается как «строковый образ значения Count в типе Integer». Атрибут 'Image — очень удобный инструмент для отладочной печати, потому что работает с любым типом, у которого есть 'Image, включая перечисления. Обратите внимание: для неотрицательных чисел 'Image добавляет ведущий пробел (место под знак), поэтому в выводе виден лишний пробел перед 42. Это известная деталь, к ней привыкают.
Чтение ввода
Симметрично выводу есть ввод. Прочитать целую строку текста можно функцией Get_Line:
with Ada.Text_IO; use Ada.Text_IO;
procedure Ask_Name is
Name : String (1 .. 100);
Last : Natural;
begin
Put ("Как вас зовут? ");
Get_Line (Name, Last);
Put_Line ("Здравствуйте, " & Name (1 .. Last));
end Ask_Name;
Здесь видна особенность строк фиксированной длины в Ada: переменная Name — это массив на 100 символов, а Get_Line заполняет его и возвращает в Last позицию последнего прочитанного символа. Поэтому печатаем мы срез Name (1 .. Last) — ровно введённую часть. (Существуют и удобные строки переменной длины — Unbounded_String — о них в разделе про составные типы.) Для чтения чисел применяют Get из числовых пакетов: Ada.Integer_Text_IO.Get (X); прочитает целое в переменную X, пропустив ведущие пробелы.
Как работает под капотом типизированный ввод-вывод
Почему так много пакетов вместо одной универсальной печати? Потому что Ada не доверяет «магии угадывания типа». Когда вы печатаете целое процедурой из Ada.Integer_Text_IO, компилятор статически знает: это целое, печатается так-то. Нет момента, когда программа в рантайме гадает, что ей подсунули. Это согласуется с сильной типизацией: тип известен заранее, операция выбрана заранее, поведение предсказуемо. Для пользовательских числовых типов (которые мы создадим в следующем разделе) Ada даже позволяет породить специализированный пакет ввода-вывода под конкретный тип через механизм generics — но это уже продвинутая тема. Главная идея сейчас: ввод-вывод честный и типизированный, а удобство (универсальная печать чего угодно) сознательно принесено в жертву предсказуемости.
Файлы и стандартные потоки: ввод-вывод шире консоли
До сих пор мы печатали в консоль, но Ada.Text_IO устроен общее: те же операции работают и с файлами. Консольный вывод — это просто частный случай вывода в «стандартный выходной поток», и под капотом Put_Line ("текст") эквивалентен записи в файл стандартного вывода. Чтобы работать с настоящим файлом на диске, объявляют объект типа File_Type, открывают или создают его и передают первым аргументом в те же Put/Get:
with Ada.Text_IO; use Ada.Text_IO;
procedure Write_Log is
Log : File_Type;
begin
Create (Log, Out_File, "report.txt"); -- создать файл для записи
Put_Line (Log, "Первая строка отчёта"); -- печать В ФАЙЛ, а не в консоль
Put_Line (Log, "Вторая строка");
Close (Log); -- обязательно закрыть
end Write_Log;
Процедура Create открывает новый файл в режиме Out_File (только запись); существующий файл открывают через Open с режимом In_File (чтение) или Append_File (дозапись). Дальше всё знакомо: Put_Line (Log, ...) — это та же процедура, что и для консоли, только с явным указанием, куда писать. Завершают работу вызовом Close, который сбрасывает буферы и освобождает файл. Симметрично, чтение из файла — это Get_Line (Log, ...) и проверка конца файла функцией End_Of_File (Log).
Здесь проявляется красивая регулярность: консоль и файл — это один и тот же интерфейс. Версия без явного файла просто использует стандартный поток по умолчанию; версия с файлом направляет тот же вывод в другое место. Не нужно учить отдельный «файловый API» — вы уже знаете Put, Put_Line, Get_Line, и они работают всюду. Это пример продуманной ортогональности: одно понятие (текстовый поток) покрывает и экран, и файлы. Для надёжных систем важно и то, что файловые операции типизированы и проверяемы: открытие несуществующего файла или чтение за концом поднимают исключения (Name_Error, End_Error), а не возвращают тихий мусор. Так даже работа с внешним миром — принципиально ненадёжная по природе — остаётся под контролем системы исключений Ada, и ошибки ввода-вывода невозможно случайно проигнорировать.
Полезно осмыслить и более общую картину: почему типизированный ввод-вывод, поначалу кажущийся неудобным, на дистанции выигрывает. В языках с «магической» универсальной печатью, угадывающей формат значения, удобство первых минут оборачивается коварством: формат выбирается неявно, и легко получить не то, что ожидал, особенно при смешении типов или локалей. Ada же требует явности — для каждого вида данных свой проверяемый способ вывода, — и потому поведение программы предсказуемо до запуска. Это та же сделка, что и со строгой типизацией в целом: немного больше явности в коде в обмен на отсутствие неприятных сюрпризов в эксплуатации. Для систем, печатающих критичные данные — телеметрию, журналы аудита, показания приборов, — предсказуемость формата важнее краткости записи. И вновь проявляется сквозной мотив языка: не доверять угадыванию, делать намерение явным, проверять как можно раньше. Даже такая будничная операция, как вывод числа, в Ada подчинена этому принципу, и потому ей можно доверять в системе, где неверно отформатированное или потерянное сообщение способно ввести в заблуждение оператора в критический момент.
Стоит добавить и о буферизации, потому что это частая причина недоумений новичка. Текстовый вывод обычно буферизуется: символы накапливаются в памяти и реально пишутся в файл или на экран порциями, ради эффективности. Из-за этого, если программа аварийно завершится, последние строки могут не успеть «дойти» до файла. Вызов Close гарантированно сбрасывает буфер на диск, поэтому закрывать файлы обязательно; для немедленного сброса без закрытия есть процедура Flush. Понимание буферизации объясняет, почему «потерялась последняя строка лога» при падении — данные были в буфере, а не на диске. В надёжных системах это учитывают, сбрасывая критичные записи сразу. Так даже такая прозаичная вещь, как печать в файл, в Ada предсказуема и управляема: вы точно знаете, когда данные гарантированно сохранены, а когда ещё в пути.
Частые ошибки и заблуждения
- Печатать число строковым
Put. Для целых нуженAda.Integer_Text_IO, для вещественных —Ada.Float_Text_IO; строковыйPutчисло не примет. - Удивляться пробелам перед числами. Целые печатаются в поле фиксированной ширины; используйте
Width => 0для компактного вывода. - Склеивать строку с числом через
&. Сначала преобразуйте число атрибутом'Image(например,Integer'Image (X)). - Забывать про
LastприGet_Line. Строки фиксированной длины: печатать нужно срез(1 .. Last), иначе выведется и «хвост» из неиспользованных позиций. - Путать
PutиPut_Line. Первый не переводит строку, второй переводит; для отдельного перевода естьNew_Line.
Итоги
- Ввод-вывод в Ada предоставляется библиотекой и типизирован: для строк —
Ada.Text_IO, для целых —Ada.Integer_Text_IO, для вещественных —Ada.Float_Text_IO. Putпечатает без перевода строки,Put_Line— с переводом;New_Lineпереводит строку отдельно.- Числа печатаются в поле фиксированной ширины; именованный аргумент
Width => 0убирает выравнивание,Fore/Aft/Expформатируют вещественные. - Строки склеиваются оператором
&; число превращается в строку атрибутом'Image. - Чтение:
Get_Lineдля строк (с возвратомLast),Getиз числовых пакетов для чисел; типизированность исключает «угадывание» формата в рантайме.