Каждый программист должен уметь работать с текстовыми файлами, файлами, содержащими записи, и файлами, которые не имеют определенной структуры или обрабатываются так, как если бы они не имели структуры. Вначале мы рассмотрим текстовые файлы, поскольку этот тип файлов используется наиболее часто.
Для получения доступа к текстовым файлам служат переменные типа Text:
var myFile: Text;
Прежде чем можно будет приступить к работе с файлом, с помощью процедуры AssignFile его нужно присвоить переменной типа Text. Эта процедура принимает два параметра: переменную файла и имя файла.
procedure AssignFile(var F; FileName: string); AssignFile(myFile, 'с:file:///C:/data/data.txt);
После того как имя файла присвоено переменной файла, необходимо определить действия, которые нужно выполнять с файлом. Файл можно подготовить к чтению, записи или дополнению.
Чтобы подготовить файл к записи, необходимо использовать процедуру Rewrite, которая всегда создаст новый пустой файл. Если файл с таким же именем файла уже существует, процедура Rewrite вначале удаляет существующий файл, а затем заменяет его новым пустым файлом. Затем процедура открывает файл и устанавливает указатель позиции на начало файла.
При работе с текстовыми файлами процедуре Rewrite необходимо передавать только переменную типа Text:
procedure Rewrite(var F: File [; Recsize: Word ]); Rewrite(myFile);
Когда файл открыт и готов к записи, для записи текста в текстовый файл можно использовать стандартную процедуру WriteLn. При выполнении записи в текстовый файл первым параметром, переданным процедуре WriteLn, должна быть переменная файла:
procedure WriteLn([ var F: Text; ] PI [, P2, ...,Pn ]); WriteLn(myFile, 'cave canem');
По завершении работы с файлом его всегда следует закрывать, чтобы обеспечить корректное сохранение файла на диске и освободить любую память, занятую в процессе записи. Для закрытия файла служит процедура CloseFile, принимающая единственный параметр — файл, который нужно закрыть:
procedure CloseFile(var F); CloseFile(myFile);
Пример программы записи строки текста в текстовый файл приведен в листинге 8.1.
Листинг 8.1. Запись текста в текстовый файл
program Project1; {$APPTYPE CONSOLE} uses SysUtils; var myFile: Text; begin AssignFile(myFile, 'c:\data.txt'); Rewrite(myFile); WriteLn(myFile, 'cave canem'); CloseFile(myFile); end.
Для подготовки файла к чтению используется процедура Reset. Эта процедура, подобно процедуреRewrite, принимает только параметр типа файла. Ее можно считать безопасной в том смысле, что она успешно работает, если дисковод и/или каталог, указанный в имени файла, существует. В отличие отRewrite, выполнение процедуры Reset будет невозможным, если файл, присвоенный переменной файла, не существует.
Для выполнения чтения данных из текстового файла можно использовать процедуру ReadLn. При этом первым передаваемым процедуре параметром должна быть переменная файла, а вторым — строковая переменная, которая будет временно хранить значение, считанное из файла.
Следующий пример демонстрирует считывание значений из текстового файла и их отображение на экране.
Листинг 8.2. Считывание текста из текстового файла
program Project1; {$APPTYPE CONSOLE} uses SysUtils; var myFile: Text; line: string; begin AssignFile(myFile, 'c:\data.txt'); Reset(myFile); ReadLn(myFile, line); WriteLn(line); CloseFile(myFile); ReadLn; end.
Этот код будет успешно работать до тех пор, пока существует файл data. txt. Если этот файл не существует, программа даст сбой. Во избежание остановки приложения при отсутствии файла необходимо выполнять проверку успешности открытия файла с помощью процедуры Reset.
Для выяснения наличия ошибок ввода-вывода необходимо непосредственно после вызова процедуры ввода-вывода, такой как Rewrite или Reset, вызвать функцию IOResult. Функция IOResult возвращает результат последней выполненной операции ввода-вывода. Если IOResult возвращает 0, это означает, что операция была выполнена успешно.
Для выполнения проверки ввода-вывода с помощью функции IOResult необходимо вначале отключить автоматическую проверку ввода-вывода. Для включения и отключения проверки ошибок ввода-вывода служит директива компилятора SI. Обычно автоматическую проверку ввода-вывода отключают перед вызовом процедуры ввода-вывода и снова включают сразу после выполнения этого вызова:
{$1-} Вызов процедуры ввода-вывода {$1 + }
Следующий пример иллюстрирует выполнение проверки ввода-вывода и считывание текста из файла только в случае успешного его открытия.
Листинг 8.3. Проверка ошибок ввода-вывода
program Project1;
{$APPTYPE CONSOLE} uses SysUtils; var myFile: Text; line: string; fileName: string; begin fileName := 'c:\data.txt'; AssignFile(myFile, fileName); {$I-} Reset(myFile); {$I+} if IOResult = 0 then begin ReadLn(myFile, line); WriteLn(line); CloseFile(myFile); end else WriteLn('Cannot open file: ', fileName); ReadLn; end.
Помните, что после обращения к процедуре ввода-вывода функцию IOResult можно вызывать только один раз. Это обусловлено тем, что она сбрасывает результат выполнения последней операции ввода-вывода в 0. Поэтому, если вызвать функцию IOResult дважды подряд, первое обращение к ней правильно сообщит об ошибке, но второе обращение сообщит (ошибочно), что операция была выполнена успешно.
Когда нужно выполнить считывание всего текстового файла, его необходимо считывать последовательно до достижения конца файла. Для определения конца файла служит функция Eof. Эта функция принимает единственный параметр файла и возвращает булевское значение, указывающее, был ли достигнут конец файла:
function Eof [ (var F: Text) ]: Boolean;
Как правило, наиболее рациональный способ считывания текстового файла предусматривает использование цикла while, продолжающего итерации вплоть до достижения конца файла. Следующий пример иллюстрирует копирование содержимого одного текстового файла в другой с использованиемцикла while not Eof (результаты можно видеть на рис. 8.1).
Листинг 8.4. Копирование текстового файла
program Project1; {$APPTYPE CONSOLE} uses SysUtils; var srcFile: Text; destFile: Text; line: string; begin { try to open the source file } AssignFile(srcFile, 'c:\data.txt'); {$I-} Reset(srcFile); {$I+} if IOResult = 0 then begin { try to open the destination file } AssignFile(destFile, 'c:\copy.txt'); {$I-} Rewrite(destFile); {$I+} if IOResult = 0 then begin { while loop that copies text from source to dest } while not Eof(srcFile) do begin ReadLn(srcFile, line); WriteLn(destFile, line); WriteLn('Copying: ', line); end; // while not Eof CloseFile(destFile); WriteLn; WriteLn('File successfully copied.'); end; // if destFile is OK CloseFile(srcFile); end; // if srcFile is OK ReadLn; end.
Рис. 8.1. Копирование текстового файла
Простейший способ считывания всего файла в память — его считывание в динамический массив строк. Для этого необходимо знать количество строк текста в файле. Поскольку никаких стандартных функций для выполнения этой задачи не существует, потребуется создать такую функцию. Для вычисления количества строк в текстовом файле необходимо в цикле while not Eof подсчитать количество строк в файле, а затем вызвать процедуру Reset, чтобы вернуться к начальной позиции файла.
В следующем примере цикл while not Eof в функции GetLineCount используется для получения количества строк текстового файла. Полученный результат затем используется в вызове функцииSetLength для изменения размера динамического массива.
Листинг 8.5. Загрузка текстового файла в динамический массив
program Project1; {$APPTYPE CONSOLE} uses SysUtils; var myFile: Text; lines: array of string; cnt: Integer; fileName: string; function GetLineCount(var ATextFile: Text): Integer; begin Result := 0; while not Eof(ATextFile) do begin ReadLn(ATextFile); Inc(Result); end; Reset(ATextFile); { move position to the beginning } end; begin fileName := 'c:\data.txt'; AssignFile(myFile, fileName); {$I-} Reset(myFile); {$I+} if IOResult = 0 then begin { resize dynamic array and load the lines into the array} SetLength(lines, GetLineCount(myFile)); for cnt := Low(lines) to High(lines) do ReadLn(myFile, lines[cnt]); { close file } CloseFile(myFile); { work with strings in memory } for cnt := Low(lines) to High(lines) do WriteLn(UpperCase(lines[cnt])); end else WriteLn('Cannot open file: ', fileName); ReadLn; end.
Как только приложение загрузило весь текстовый файл в динамический массив, файл можно закрыть и продолжить работу со строками в памяти. Конечно, со строками в массиве можно выполнять любые действия, но этот код всего лишь с помощью функции Uppercase преобразует (временно) символы строк в прописные и отображает их на экране.
Типизированные файлы представляют собой двоичные файлы, которые содержат элементы одинакового размера. Обычно типизированные файлы — это файлы, содержащие записи. Чтобы создать типизированный файл, вначале, используя следующий синтаксис, необходимо создать новый файловый тип данных:
type NewFileType=file of DataType;
В следующем коде продемонстрировано создание нового файлового типа, который можно использовать для считывания и записи записей в типизированном файле:
type TPerson = record FirstName: string[20]; LastName: string[30]; Age: Integer; end; TPersonFile = file of TPerson;
Обратите внимание, что строковые поля в объявлении записи имеют явно определенную длину. Длина строки должна быть определена явно, поскольку размер всей записи должен оставаться постоянным. Если требуется сохранить запись в файле на диске, обычные строки использовать нельзя, поскольку их длина может изменяться в любое время и компилятор не может определить их длину во время компиляции.
Существует несколько различий между текстовыми и типизированными файлами:
- При сбросе типизированного файла в нем можно выполнять считывание и запись (при сбросе текстового файла в нем можно выполнять только считывание).
- При выполнении считывания или записи из типизированного файла необходимо использовать процедуры Read и Write, а не ReadLn и WriteLn.
В листинге 8.6 демонстрируется работу с типизированными файлами.
Листинг 8.6. Работа с типизированными файлами
program Project1;
{$APPTYPE CONSOLE} uses SysUtils; type TPerson = record FirstName: string[20]; LastName: string[30]; Age: Integer; end; TPersonFile = file of TPerson; procedure ReadRecord(const AFileName: string; var Rec: TPerson); var F: TPersonFile; begin AssignFile(F, AFileName); {$I-} Reset(F); {$I+} if IOResult = 0 then begin Read(F, Rec); CloseFile(F); end; // if IOResult end; procedure WriteRecord(const AFileName: string; var Rec: TPerson); var F: TPersonFile; begin AssignFile(F, AFileName); Rewrite(F); Write(F, Rec); CloseFile(F); end; procedure DisplayRecord(var Rec: TPerson); begin WriteLn('First Name: ', Rec.FirstName); WriteLn('Last Name: ', Rec.LastName); WriteLn('Age: ', Rec.Age); WriteLn; end; var TestRec: TPerson; ReadRec: TPerson; begin TestRec.FirstName := 'Stephen'; TestRec.LastName := 'King'; TestRec.Age := 58; WriteRecord('c:\info.dat', TestRec); ReadRecord('c:\info.dat', ReadRec); DisplayRecord(ReadRec); ReadLn; end.
Еще одно существенное различие между текстовыми и типизированными файлами состоит в том, что в то время как текстовые файлы допускают только последовательный доступ, типизированные файлы допускают произвольный доступ к записям файла. Произвольный доступ возможен вследствие того, что все записи в файле имеют одинаковый размер, поэтому для считывания конкретной записи процедуре достаточно пропустить определенное, легко определяемое количество байт.
Для определения количества записей в файле можно использовать функцию FileSize. Для перехода к определенной записи в файле можно использовать процедуру Seek. Эта процедура принимает два параметра: переменную файла и целочисленное значение, указывающее номер записи (начиная с О), к которой необходимо выполнить переход.
var F: TPersonFile; RecCount: Integer; begin RecCount := FileSize(F); if RecCount = 0 then WriteLn(<sup>1</sup> Файл пуст') else Seek(F, FileSize(F)); end.
Нетипизированные файлы — это файлы без определенной структуры. В общем случае, нетипизированные файлы — это типизированные файлы, в которых вместо записей используются байты. Объявление переменной нетипизированного файла выглядит следующим образом:
var F: file;
При работе с нетипизированными файлами обращения к процедурам Reset и Rewrite выглядят несколько иначе. Обычно обе эти процедуры в качестве заданного по умолчанию размера записи используют 128байт. При работе с нетипизированными файлами этот размер должен быть установлен равным 1 байту. Это можно выполнить, передавая 1 в качестве второго параметра в обоих вызовах:
Reset (F, 1); Rewrite(F,1);
Для считывания и записи данных в нетипизированные файлы применяются процедуры BlockRead иBlockWrite. Объявления этих процедур показаны ниже:
procedure BlockRead(var F: File; var Buf; Count: Integer [; var AmtTransferred : Integer]); procedure BlockWrite(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);
Первый параметр — переменная нетипизированного файла, используемая для доступа к файлу на диске. Второй параметр — буфер, который процедуры используют для передачи данных в файл и из него.
Обычно этот буфер представляет собой статический массив байтов, но он может быть также и записью. Третий параметр указывает количество передаваемых байтов. Обычно это число равно размеру массива, который легко определить с помощью функции SizeOf. Необязательный параметр-переменнаяAmtTransferred можно использовать для отслеживания точного числа байтов, переданных в файл и из него.
Приложение, код которого представлен в листинге 8.7, использует процедуры BlockRead и BlockWrite для предоставления пользователю возможности копирования файлов. Приложение также дает пользователю возможность указывать имена исходного и целевого файлов в командной строке, как показано на рис. 8.2.
Рис. 8.2. Передача параметров приложению
Листинг 8.7. Копирование файлов с помощью процедур BlockRead и BlockWrite
program Project1; {$APPTYPE CONSOLE} uses SysUtils; procedure BlockCopyFile(const SrcPath, DestPath: string); var Src: file; Dest: file; Buffer: array[1..1024] of Byte; BytesRead: Integer; begin if LowerCase(SrcPath) = LowerCase(DestPath) then Exit; AssignFile(Src, SrcPath); {$I-} Reset(Src, 1); {$I+} if IOResult = 0 then begin AssignFile(Dest, DestPath); {$I-} Rewrite(Dest, 1); {$I+} if IOResult = 0 then begin BytesRead := -1; while BytesRead <> 0 do begin BlockRead(Src, Buffer, SizeOf(Buffer), BytesRead); BlockWrite(Dest, Buffer, BytesRead); end; CloseFile(Dest); WriteLn('File successfully copied.'); end; // if Dest Rewrite CloseFile(Src); end; // if Source Reset end; var SourcePath: string; DestPath: string; begin { Accept parameters } if ParamCount = 2 then BlockCopyFile(ParamStr(1), ParamStr(2)) else begin { if there are no parameters in the command line, ask the user to enter filenames here } Write('Source path: '); ReadLn(SourcePath); Write('Destination path: '); ReadLn(DestPath); if (SourcePath <> '') and (SourcePath <> '') then BlockCopyFile(SourcePath, DestPath); end; WriteLn('Press Enter to exit.'); ReadLn; end.
Для выяснения количества параметров, которые пользователь передал приложению, применяется функция ParamCount. Эта функция не принимает никаких параметров и возвращает значение 0. если пользователь вызвал приложение без дополнительных параметров.
Для считывания параметров, переданных приложению, служит функция ParamStr, которая принимает единственный параметр типа Integer — индекс параметра. Если передать этой функции значение О, она возвратит путь и имя файла приложения. Индексы пользовательских параметров, если они переданы, начинаются с 1.
Вначале приложение проверяет, передал ли пользователь два параметра в командной строке. Если да, то ParamStr (1) содержит путь к исходному файлу, а ParamStr (2) —путь к файлу назначения.
Копирование выполняется в процедуре BlockCopyFile.
Ее первая строка:
if Lowercase(SrcPath) = Lowercase(DestPath) then Exit;
использует функцию LowerCase для временного преобразования имен обоих файлов в строчные буквы и проверяет, указывают ли оба имени файлов на один и тот же файл. Если файл назначения и файл-источник совпадают, никакое копирование не требуется и оператор if-then вызывает процедуру Exit для выхода из процедуры.
Основная часть процедуры BlockCopyFile — цикл while, который вызывает процедуры BlockRead иBlockWrite:
while BytesRead о 0 do begin BlockRead(Src, Buffer, SizeOf(Buffer), BytesRead); BlockWrite(Dest, Buffer, BytesRead); end;
Процедура BlockRead считывает из файла 1 Кбайт данных, записывает эти данные в массив Buffer и обновляет переменную BytesRead, которая всегда содержит точное количество переданных байтов. Выполнение цикла будет повторяться до тех пор, пока процедура BlockRead продолжает считывать данные из файла. Когда эта процедура достигнет конца файла, значение переменной BytesRead будет сброшено в 0, условие цикла while станет ложным и копирование файла завершится.