Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лабораторная работа 10.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
142.93 Кб
Скачать

Лабораторная работа №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. Для этого нужно выбрать пункт меню FileNewFile…, в открывшемся диалоговом окне создания нового файла выбрать тип “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);

}