Этот пост посвящен рабочему моему инструменту: фортрану. Если что, это весьма живой и действующий язык, новый стандарт вышел в 2023 году. Если вам фортран неинтересен, можете дальше не читать.
А мы разберем шесть способов читать из файла или записывать в него.
Вообще, в фортране есть три "доступа" (access):
- последовательный (sequential)
- прямой (direct)
- потоковый (stream).
И две "формы":
- форматный доступ (formatted)
- неформатный (unformatted).
Каждый с каждым, вот и получается шесть.
Открывая файл, мы указываем и доступ, и форму (но можно положиться на умолчание):
open(42, file='file.txt', access='sequential', form='formatted')
последовательный форматный. Можно опустить два последних спецификатора, это выбор по умолчанию.
open(42, file='file.txt', access='direct', form='unformatted', recl=666)
прямой неформатный, предпоследний спецификатор можно опустить.
open(42, file='file.txt', access='stream', form='unformatted')
потоковый неформатный, также по умолчанию будет неформатный.
Зачем так много и что это всё значит?
Начнем с формы. Форматированный ввод-вывод - это данные в текстовом виде. Формат определяет, как данные записывать или как их читать. Сколько цифр после точки, дополнять ли слева нулями, или пробелами, или не дополнять, писать ли знак "плюс", сколько символов выделить на число или текст, и так далее.
Неформатный - это байты. Точнее, единицы хранения, которые могут быть, например, четыре байта, если так удобнее писать на диск. В принципе, целое 4 байта, вещественное одинарной точности тоже 4, двойной точности 8, даже логическое булево значение и то 4 байта.
Последовательный доступ к файлу - это через записи, и файл представлен как лента - используются термины вроде перемотки (rewind). Записи могут быть разной длины, в файл записывается какой-то маркер конца записи. В простом случае вы просто пишете записи по порядку, с начала файла или дописываете в конец, либо читаете, также по порядку. Пре необходимости можно перемотать в начало или в конец, отмотать одну или несколько записей назад или вперед, и так далее.
Последовательный доступ хорошо годится для форматного ввода-вывода, а записи - это строки. Признаком конца записи служит символ конца строки. Строки, разумеется, могут быть разной длины. И в большинстве случаев пишутся и читаются по порядку, но иногда надо перейти в конец файла (это можно даже разными способами), в начало, а реже надо просто сместиться, например, на одну строку вверх и перезаписать ее.
Важно! Если вы перешли в начало файла и начали писать, то всё в файле пропадет и будет перезаписано! В том числе и если вы открыли файл и начали туда писать: распространенная ошибка.
Если вы открыли существующий файл и стали оттуда читать, то всё будет как вы ожидаете: строки-записи будут читаться по одной, одна за другой, с начала файла. А если вы будете записывать, то всё от текущей строки до конца файла будет затерто! Это удобно, если вам и надо начать файл сызнова, но неприятно, если вы думали просто перезаписать первую строку.
Что такое последовательный неформатный доступ? Это те же записи, только в двоичной форме. Это редко нужно, но бывает полезно - если вам надо хранить данные в разнородных записях. Причем много данных, так как если мало, то нет причин отказываться от текстовой формы. Но если данных много, то двоичная форма все-таки экономнее. Так, в 4 байта запихнете максимум 3.14, что мало даже для половинной точности, а в двоичной форме все-таки 6-7 знаков будет н ате же четыре байта.
Случается, что "старая гвардия" пользуется неформатной последовательной схемой там, где надо использовать прямой доступ. Это крайне старомодно. Да, тот же GrADS может такое читать, и в целом разные компиляторы справляются с файлами, записанными другими реализациями (хотя стандарт не обязывает!), но это неэкономно, ошибкоёмко, нелогично, непоследовательно и в целом неправильно.
Что же такое прямой доступ? Это тоже записи, только одного размера и доступ к ним прямой: по номеру. Это очень полезно при чтении, но и при записи тоже бывает удобно.
Так, если у вас в файле трехмерные массивы средней температуры за сутки за весь год, то у вас там 365 одинаковых трехмерных массивов. И вы можете читать любой просто по номеру.
И записывать тоже. Конечно, здесь есть ловушка: если вы запишете данные на 31 декабря, то есть последнюю запись, то проблем никаких, файл будет создан. И размер у него будет "какой следует", при этом 364 записи там "мусорные", и пока вы их не запишете - лучше оттуда ничего не читать.
При записи или чтении надо указать спецификатор rec, который задает номер записи:
WRITE(42, rec=6) A
READ(42, rec=365) B
При открытии файла оператором open надо указать, помимо номера устройства (это числовой идентификатор файла), имени файла, доступа и форматности, еще и спецификатор RECL, который задает размер записи в единицах хранения.
Размер этот можно получить, перемножив размерности массива и умножив результат на размер одного элемента в единицах хранения. Для этого надо знать размер элемента в байтах и размер единицы хранения в байтах же. Причем размер единица храненя реально разный в разных компиляторах: где-то 1 байт, где-то 4. Поэтому лучше использовать оператор INQUIRE:
inquire(iolength=iol) array(:,:,1)
open(42, file='file.txt', access='direct', form='unformatted', recl=iol)
Здесь iol - переменная целого типа. Там будет правильный размер двумерного массива.
Если вы пишете, скажем, массивы 100 на 100 чисел одинарной точности (4 байта на каждое), то каждая запись будет ровно 40000 байт. Если в файле их 10, то файл будет ровно 400 000 байт. То есть никаких сложностей с чтением такого файла не будет, если только ваши системы правильно понимают устройство числа REAL (или float, или как вы там его называете) и размерности заданы одинаково. В файле нет ничего лишнего: если 10 тысяч чисел по 4 байта на каждое, то там только эти сорок тысяч байт, и ничего больше.
Существует ли форматный прямой доступ? Конечно. Но используется редко. Это строки одинаковой длины, записываемые по номеру. Такой доступ позволит, например, перезаписать первую строку файла, что бывает полезно. Скажем, сложный трудоемкий алгоритм создал некоторый файл соответствия ячеек - которые соседствуют с которой. И по ходу вы выяснили, какое максимальное число соседок вообще есть. И хотите это число вписать в начало файла, строкой типа: MAX NEIGHBOURS: 42. Строка должна уже быть, но перезаписать ее - теперь вы знаете, как.
Теперь потоковый доступ. Он был введен в стандарте Fortran-2003, в том же, в котором ввели объектность. И нужен он для того, чтобы читать (и писать) всякие нефортрановские форматы файлов, вроде Excel. По сути, это доступ к отдельным байтам.
Естественной формой является поэтому неформатная. Всё очень похоже на прямую запись, только вместо rec спецификатор pos и он указывает номер единицы хранения. Узнать, где вы остановились, позволяет INQUIRE со спецификатором pos.
Понятное дело, что что вы там читаете - это ваше дело. Если вы записали целое число (4 байта) как вещественное, то и прочитаете вы непонятно что. Впрочем, с неформатным доступом это всегда так.
В целом, ситуация мало отличается от прямого доступа с записями по одной единице хранения. Но отличается. Так, вы можете потоково записать целый массив, а потом через INQUIRE узнать, где остановились. Прямым доступом придется писать массив по одному элементу в цикле, что и дольше, и менее наглядно.
Есть и потоковый форматный доступ. Если отождествлять байт и символ, что несовременно, но в пределах английского алфавита допустимо, то это то же самое, только вместо байтов символы. Вы можете по номеру прочитать или записать символ, ну и определить, какова текущая позиция. Если же у вас UTF8, то читаться текст будет по одному байту. Это нормально, если вы всё делаете грамотно, но получить "обрывок" вполне реально. Фортран не для обработки юникодного текста, хотя какие-то меры в этом направлении принимаются.
Подводя итоги, ответим на вопросы: зачем нужен каждый из шести стилей доступа.
- Форматный последовательный - для вывода текста на STDOUT или в текстовый файл, строка за строкой, либо для чтения текста из файла, строка за строкой. Нужно часто и повсевместно. По умолчанию: open(1, file='file.txt')
- Неформатный прямой - для вывода данных в двоичном виде, каждый массив это запись, пишутся или читаются по номеру независимо от остальных. Нужен часто. По умолчанию для прямого доступа. При открытии надо указать размер записи в единицах хранения, правильно использовать INQUIRE(iolength=var) МАССИВ.
- Неформатный потоковый - для чтения и записи отдельных единиц хранения, по сути байт (может быть, четверок байт, но разберетесь). Нужен для реализации доступа к сторонним двоичным форматам.
- Неформатный последовательный - в целом, архаика. Может пригодиться, если нужно записывать (или читать) сложные объемные структуры данных с записями переменной длины.
- Форматный прямой - экзотика. Нужен для перезаписи отдельных строк в готовом файле. Перемотка в последовательном доступе не годится! Она уничтожит все записи после перезаписываемой.
- Форматный потоковый - тоже экзотика. Доступ к тексту по одному символу. При записи может вставлять концы строк, что несколько запутывает. Но обращение к символу по pos всегда однозначно. Просто может оказаться так, что вывод символа в позицию 1 делает текущей позицию 3, так как в позиции 2 символ "конец строки".
В целом, в прямых руках все шесть способов рабочие. Но реально нужны первые два. Остальное - если вы знаете, что делаете.