
Лабораторная работа №10. Основы работы с файлами в c#
Работа с файлами в языке C# реализуется посредством потоков. Поток – это абстрактное понятие, описывающее любой перенос данных от источника к приемнику. Именно потоки позволяют считывать данные из файла и записывать данные в файл. Поток представляет собой последовательность байтов и не зависит от конкретного устройства, с которым производится обмен (оперативная память, файл на диске, клавиатура, принтер).
Для поддержки потоков библиотека .NET содержит иерархию классов, основная часть которой представлена на рис. 10.1.
Отметим, что эти классы определены в пространстве имен System.IO, поэтому чтобы воспользоваться объектами и методами этих классов, необходимо указать пространство имен System.IO в области директивы using в начале программы.
Рассмотрим некоторые из перечисленных выше классов, необходимые для организации работы с фалами, подробнее.
Классы TextReader и StreamReader
В языке C# такие операции как считывание данных из файла и запись данных в файл реализованы на основе манипуляций с байтами. Однако в связи с тем, что человеку гораздо более удобно воспринимать информацию, представленную в символьном формате, в библиотеке .NET разработаны классы StreamReader и StreamWriter, которые позволяют преобразовывать байтовые потоки в символьные и наоборот.
Как видно из рисунка 10.1, класс StreamReader является потомком абстрактного базового класса TextReader, предоставляющего классам-наследникам набор методов, реализующих считывание последовательности символов или строк из потока (файла). Некоторые из методов приведены в таблице 10.1.
Таблица 10.1
Некоторые методы класса TextReader
Название |
Описание |
Peek() |
Возвращает следующий символ из потока, при этом указатель текущей позиции не перемещается (удобно использовать для проверки наличия следующего символа в потоке: если конец файла достигнут, возвращаемое значение равно –1) |
Read() |
Первый вариант: считывает текущий символ из потока и передвигает указатель на один символ в соответствии с кодировкой. Если доступных для считывания символов нет, возвращает значение –1. Второй вариант: считывает из потока последовательность символов и записывает ее в массив символов, начиная с определенного индекса. Метод возвращает фактическое количество считанных символов, либо нуль при отсутствии символов, доступных для чтения |
ReadBlock() |
Блокирующая версия метода Read() |
ReadLine() |
Считывает строку символов из текущего потока и возвращает данные в строковом формате либо значение null, если достигнут конец потока входных данных |
ReadToEnd() |
Считывает все символы, начиная с текущей позиции, до конца потока как одну строку |
Поясним разницу между блокирующими и неблокирующими методами чтения. Обмен с потоком для повышения скорости передачи данных производится, как правило, через специальную область оперативной памяти, называемую буфер. Буфер выделяется для каждого открытого файла. При чтении из файла данные вначале считываются в буфер, причем не столько, сколько запрашивается, а сколько помещается в буфер, а уже из буфера передаются в поток. Таким образом, в процессе считывания, возможна ситуация, когда буфер пуст, т. е. данные пока не доступны, но ожидаемы в будущем. Именно в такой ситуации блокирующие и неблокирующие методы ведут себя по-разному: первые блокируют процесс считывания, помещая его в режим сна до тех пор, пока не появятся доступные для чтения данные, в то время как вторые просто возвращают в процесс некоторое значение, указывающее на то, что доступных для чтения данных нет.
Для того чтобы получить доступ к любому из описанных выше методов, необходимо создать экземпляр класса StreamReader с помощью конструктора StreamReader(<ИмяФайла>), при этом создается экземпляр класса StreamReader и связывается с конкретным физическим файлом. В качестве параметра можно указать либо только имя файла и в этом случае файл должен находиться в папке “…\bin\debug” текущего проекта, либо полностью путь и имя файла. Пример использования:
try
{
StreamReader InputFile = new StreamReader("input.txt");
while (InputFile.Peek() != -1)
Console.Write((char)InputFile.Read());
InputFile.Close();
}
catch
{
Console.WriteLine("Ошибка чтения файла");
}
Здесь мы создаем экземпляр класса StreamReader и связываем его с файлом “input.txt”. Далее в цикле while считываем посимвольно данные из файла с помощью метода Read() и выводим их на экран. Обратите внимание на то, что метод Read() возвращает значение считанного символа в формате int, поэтому мы преобразуем его в символьный формат. После того как конец файла достигнут, необходимо освободить ресурсы памяти, выделенные потоку, с помощью метода Close(). Оператор try позволяет обработать ошибку, которая может возникнуть при работе с файлом, например, в случае если файл не найден.
Напомним также о необходимости указать пространство имен System.IO в области директивы using в начале программы, иначе компилятор не сможет распознать встроенный класс StreamReader.
Для того чтобы проверить работоспособность программы, необходимо создать текстовый файл ”input.txt” и сохранить его в папке “…\bin\debug” текущего проекта. Это можно сделать в любом текстовом редакторе, но удобнее – непосредственно в среде Visual Studio. Для этого нужно выбрать пункт меню File – New – File…, в открывшемся диалоговом окне создания нового файла выбрать тип “Text File”, а далее ввести в файл данные и сохранить его.
Отметим, что альтернативой методу Close() может служить использование конструкции using, в этом случае занимаемая потоком память освобождается автоматически при выходе из конструкции. Пример:
try
{
using (StreamReader InputFile = new
StreamReader(@"c:\work\input.txt"))
while (InputFile.Peek() != -1)
Console.WriteLine(InputFile.ReadLine());
}
catch
{
Console.WriteLine("Ошибка чтения файла");
}
Здесь мы считываем данные из файла построчно и выводим их на экран. Обратите внимание, что путь к файлу указан полностью, при этом символ ‘@’ позволяет блокировать управляющие символы.
Отдельно рассмотрим в качестве примера заполнение одномерного и двумерного массивов из файла.
В процессе заполнения одномерного массива из файла следует учитывать следующую особенность: фактическая размерность массива, т. е. количество элементов массива, записанных в файл, заранее неизвестно. Отсюда при инициализации массива мы указываем предельно допустимую размерность массива MaxArraySize, а фактическую размерность ArraySize определяем в процессе чтения из файла. Все остальные операции с массивом, такие как вывод или просмотр элементов массива, нужно выполнять, учитывая не максимально допустимую, а фактическую размерность массива.
Итак, опишем метод ReadFromFile(), реализующий заполнение целочисленного одномерного массива MyArray из заданного файла. Исходя из условия задачи, в качестве параметров данного метода введем, во-первых, имя файла FileName, во-вторых, одномерный массив MyArray, и, наконец, фактическую размерность массива ArraySize, определяемую в теле метода.
static void ReadFromFile(string FileName, int[] Arr, ref int ArrSize)
{
try
{
using (StreamReader InputFile = new StreamReader(@FileName))
{
ArrSize = 0;
while (InputFile.Peek() != -1)
{
Arr[ArrSize] =
int.Parse(InputFile.ReadLine());
ArrSize++;
}
}
}
catch
{
Console.WriteLine("Ошибка чтения файла");
}
}
В основной программе необходимо описать и проинициализировать одномерный массив с учетом максимально допустимой размерности массива MaxArraySize и вызвать описанный выше метод ReadFromFile().
static void Main()
{
const int MaxArraySize = 1000;
int[] MyArray = new int[MaxArraySize];
int ArraySize=0;
ReadFromFile("input.txt", MyArray, ref ArraySize);
}
Прежде чем запустить программу, необходимо создать текстовый файл “input.txt”, заполнить его целыми числами и сохранить в папке “…\bin\debug” текущего проекта. Важно отметить, что в программе используется построчное считывание данных с помощью метода ReadLine(), поэтому данные во входном файле должны быть введены не в одну строчку, а в столбец (см. рис. 10.2).
При выполнении этого программного кода мы указываем максимальный размер массива MaxArraySize, но это имеет два недостатка. Во-первых, если указать максимальный размер меньше, чем действительное количество элементов в файле, то произойдет ошибка. Во-вторых, если указывать слишком большое значение MaxArraySize, то будет выделяться место в памяти под элементы массива, которые никогда не будут использоваться.
Из этой ситуации имеется такой выход: сначала прочитать файл и узнать, сколько действительно элементов он содержит, а уже потом выделять место в памяти под элементы массива. Метод Main() в таком случае содержит операторы:
int[] MyArray;
ReadFromFile("input.txt", out MyArray);
Метод ReadFromFile() изменяется так:
static void ReadFromFile(string FileName, out int[] MyArray)
{
try
{
int ArraySize = 0;
using (StreamReader InputFile = new StreamReader(@FileName))
{ //подсчет количества элементов в файле
while (InputFile.Peek() != -1)
{
InputFile.ReadLine();
ArraySize++;
}
}
//выделение памяти под необходимое число элементов
MyArray = new int[ArraySize];
using (StreamReader InputFile = new StreamReader(@FileName))
{ //инициализация массива
for (int i = 0; i < ArraySize; i++)
MyArray[i] = int.Parse(InputFile.ReadLine());
}
}
catch
{
Console.WriteLine("Ошибка чтения файла");
MyArray = null;
}
}
Заметьте, что массив передается как выходной параметр с использованием ключевого слова out. Поскольку нет гарантии, что операторы в контролируемом блоке try выполнятся без ошибок, блок catch должен содержать оператор, инициализирующий выходной параметр – массив (мы присваиваем значение null).
Этот способ считывания одномерного массива из файла также имеет недостаток – в этом случае по файлу приходится проходить дважды. Как это часто бывает при написании программ, разработчик должен сделать выбор между скоростью работы программы и объемом занимаемой памяти.
Альтернативой предложенному варианту решения является использование метода ReadAllLines() класса File, который считывает все строки файла в массив строк. В этом случае второй проход по файлу не потребуется.
При заполнении двумерного массива воспользуемся таким способом (его можно применить и при работе с одномерным массивом): фактическую размерность массива, т. е. количество строк и столбцов, зададим в начале входного файла, а далее укажем значения его элементов (см. рис. 10.3), поэтому в списке параметров метода ReadFromFile() при передаче в метод двумерного массива MyArray необходимо также указать директиву out, позволяющую инициализировать массив не в основной программе, а непосредственно в теле метода, после считывания его фактической размерности из файла. Отметим, что процесс считывания данных осуществляется построчно с помощью метода ReadLine(). Считанная строка разбивается с помощью метода Split() на массив строк Row. Элементы этого массива переписываются в результирующую матрицу MyArray, преобразуясь из строк в целые числа.
static void ReadFromFile(string FileName, out int[,] MyArray)
{
try
{
using (StreamReader InputFile = new StreamReader(FileName))
{
int RowsCount = int.Parse(InputFile.ReadLine());
int ColsCount = int.Parse(InputFile.ReadLine());
MyArray = new int[RowsCount, ColsCount];
for (int i = 0; i < RowsCount; i++)
{
string[] Row = InputFile.ReadLine().Split();
for (int j = 0; j < ColsCount; j++)
MyArray[i, j] = int.Parse(Row[j]);
}
}
}
catch
{
Console.WriteLine("Ошибка чтения файла");
MyArray = null;
}
}
static void Main(string[] args)
{
int[,] MyArray;
ReadFromFile("input.txt", out MyArray);
}