Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Zuev_programmirovanie.doc
Скачиваний:
0
Добавлен:
01.03.2025
Размер:
357.38 Кб
Скачать

10. Работа с файлами

Под файлом обычно подразумевается именованная информация на внешнем носителе, например на жестком или гибком магнитном диске. Логически файл можно представить как конечное количество последовательных байтов, поэтому такие устройства, как дисплей, клавиатура и принтер, также можно рассматривать как частные случаи файлов. Передача данных с внешнего устройства в оперативную память называется чтением, или вводом, обратный процесс — записью, или выводом. Ввод-вывод в С# выполняется с помощью подсистемы ввода-вывода и классов библиотеки .NET . В этой главе рассматривается обмен данными с файлами и их частным случаем — консолью. Обмен данными реализуется с помощью потоков.

Поток (stream) —это абстрактное понятие, относящееся к любому переносу данных от источника к приемнику. Потоки обеспечивают надежную работу как со стандартными, так и с определенными пользователем типами данных, а также единообразный и понятный синтаксис. Поток определяется как последовательность байтов и не зависит от конкретного устройства, с которым производится обмен (оперативная память, файл на диске, клавиатура или принтер). Обмен с потоком для повышения скорости передачи данных производится, как правило, через специальную область оперативной памяти — буфер. Буфер выделяется для каждого открытого файла. При записи в файл вся информация сначала направляется в буфер и там накапливается до тех пор, пока весь буфер не заполнится. Только после этого или после специальной команды сброса происходит передача данных на внешнее устройство. При чтении из файла данные вначале считываются в буфер, причем не столько, сколько запрашивается, а сколько помещается в буфер. Механизм буферизации позволяет более быстро и эффективно обмениваться информацией с внешними устройствами.

Для поддержки потоков библиотека .NET содержит иерархию классов. Эти классы определены в пространстве имен System. Помимо классов там описано большое количество перечислений для задания различных свойств и режимов.

Классы библиотеки позволяют работать в различных режимах с файлами, каталогами и областями оперативной памяти.

В .NET используется кодировка Unicode, в которой каждый символ кодируется двумя байтами. Классы, работающие с текстом, являются оболочками классов, использующих байты, и автоматически выполняют перекодирование из байтов в символы и обратно. Двоичные и байтовые потоки хранят данные в том же виде, в котором они представлены в оперативной памяти, то есть при обмене с файлом происходит побитовое копирование информации. Двоичные файлы применяются не для просмотра их человеком, а для использования в программах. Доступ к файлам может быть последовательным, когда очередной элемент можно прочитать (записать) только после аналогичной операции с предыдущим элементом, и произвольным, или прямым, при котором выполняется чтение (запись) произвольного элемента по заданному адресу. Текстовые файлы позволяют выполнять только последовательный доступ, в двоичных и байтовых потоках можно использовать оба метода. Прямой доступ в сочетании с отсутствием преобразований обеспечивает высокую скорость получения нужной информации.

Помимо перечисленных классов в библиотеке .NE T есть классы XmlTextReadf и XmlTextWriter, предназначенные для формирования и чтения кода в форма XML.

Потоки байтов

Ввод-вывод в файл на уровне байтов выполняется с помощью класса FiI eStream, который является наследником абстрактного класса Stream, определяющего набор стандартных операций с потоками.

Класс FileStream реализует эти элементы для работы с дисковыми файлами. Для определения режимов работы с файлом используются стандартные перечисления FileMode, FileAccess и FileShare .

Текущая позиция в потоке первоначально устанавливается на начало файла (для любого режима открытия, кроме Append) и сдвигается на одну позицию при записи каждого байта. Для установки желаемой позиции чтения используется метод Seek, имеющий два параметра: первый задает смещение в байтах относительно точки отсчета, задаваемой вторым. Точки отсчета задаются константами перечисления SeekOrigin : начало файла — Begin, текущая позиция — Current и конец файла — End. В данном примере файл создавался в текущем каталоге. Можно указать и полный путь к файлу, при этом удобнее использовать дословные литералы, речь о которых шла в разделе «Литералы » например:

FileStream f = new FileStreamC @"D:\CJ\test.txt" ,

FileMode.Create, FileAccess.ReadWrite ):

В дословных литералах не требуется дублировать обратную косую черту. Операции по открытию файлов могут завершиться неудачно, например, при ошибке в имени существующего файла или при отсутствии свободного места на диске, поэтому рекомендуется всегда контролировать результаты этих операций. В случае непредвиденных ситуаций среда выполнения генерирует различные исключения, обработку которых следует предусмотреть в программе, например:

• FileNotFoundException, если файла с указанным именем в указанном каталоге не существует;

• DirectoryNotFoundException, если не существует указанный каталог;

• Argument Except ion, если неверно задан режим открытия файла;

• IOException, если файл не открывается из-за ошибок ввода-вывода.

Возможны и другие исключительные ситуации.

Удобно обрабатывать наиболее вероятные ошибки раздельно, чтобы предоставить пользователю программы в выводимом сообщении наиболее точную информацию. В приведенном далее примере отдельно перехватывается ошибка в имени файла, а затем обрабатываются все остальные возможные ошибки.

Асинхронный ввод-вывод

Класс Stream (и, соответственно, FileStream) поддерживает два способа выполнения операций ввода-вывода: синхронный и асинхронный. По умолчанию файлы открываются в синхронном режиме, то есть последующие операторы выполняются только после завершения операций ввода-вывода. Для длительных файловых операций более эффективно выполнять ввод-вывод асинхронно, в отдельном потоке выполнения. При этом в первичном потоке можно выполнять другие операции. Для асинхронного ввода-вывода необходимо открыть файл в асинхронном режиме, для этого используется соответствующий вариант перегруженного конструктора. Асинхронная операция ввода инициируется с помощью метода BeginRead. Помимо характеристик буфера, в который выполняется ввод, в этот метод передается делегат, задающий метод, выполняемый после завершения ввода. Этот метод может инициировать обработку полученной информации, возобновить операцию чтения или выполнить любые другие действия, например, проверить успешность ввода и сообщить о его завершении. Обычно в этом методе вызывается метод EndRead, который завершает асинхронную операцию. Аналогично выполняется и асинхронный вывод. В листинге 11.2 приведен пример асинхронного чтения из файла большого объема и параллельного выполнения диалога с пользователем.

Для удобства восприятия операции чтения из файла и диалога с пользователем оформлены в отдельный класс Demo. Метод OnCompletedRead (оператор 1) должен получать один параметр стандартного типа IAsyncResult, содержащий сведения о завершении операции, которые передаются в метод EndRead. Файл открывается в асинхронном режиме, об этом говорит значение true последнего параметра конструктора (оператор 2) . В операторе 3 создается экземпляр стандартного делегата AsyncCallback, который инициализируется методом OnCompletedRead. С помощью этого делегата метод OnCompletedRead передается в метод'BeginRead (оператор 4) , который создает отдельный поток, начинает асинхронный ввод и возвращает управление в вызвавший поток. Обратный вызов метода OnCompletedRead происходит при завершении операции ввода. При достаточно длинном файле verybigfile можно убедиться, что приглашение к вводу в методе Userlnput выдается раньше, чем сообщение о завершении операции ввода из метода OnCompletedRead.

Потоки символов

Символьные потоки StreamWriter и StreamReader работают с Unicode-символами, следовательно, ими удобнее всего пользоваться для работы с файлами, предназначенными для восприятия человеком. Эти потоки являются наследниками классов TextWriter и TextReader соответственно, которые обеспечивают их большей частью функциональности.

На содержимое файла накладываются весьма строгие ограничения: числа должны быть разделены ровно одним пробелом, после последнего числа в строке пробела быть не должно, файл не должен заканчиваться символом перевода строк. Методы разбиения строки и преобразования в целочисленное представлен рассматривались ранее.

Двоичные потоки

Двоичные файлы хранят данные в том же виде, в котором они представлены в оперативной памяти, то есть во внутренней форме представления. Двоичные файлы применяются не для просмотра их человеком, а для использования в программах. Выходной поток BinaryWriter поддерживает произвольный доступ, то есть имеется возможность выполнять запись в произвольную позицию двоичного файла. Двоичный файл открывается на основе базового потока, в качестве которого чаще всего используется поток FileStream. Входной двоичный поток содержит перегруженные методы чтения для всех простых встроенных типов данных.

При создании двоичного потока в него передается объект базового потока. При установке указателя текущей позиции в файле учитывается длина каждого значения типа doubl е — 8 байт.

Попытка просмотра сформированного программой файла в текстовом редакторе весьма медитативная, но не информативная.

При чтении принимается во внимание тот факт, что метод ReadDoubl е при обнаружении конца файла генерирует исключение EndOfStreamException. Поскольку в данном случае это не ошибка, тело обработчика исключений пустое.

Консольный ввод-вывод

Консольные приложения имеют весьма ограниченную область применения, самой распространенной из которых является обучение языку программирования. Для организации ввода и вывода используется известный вам класс Console, определенный в пространстве имен System. В этом классе определены три стандартных потока: входной поток Console. In класса TextReader и выходные потоки Consol е. Out и Console.Error класса TextWriter. По умолчанию входной поток связан с клавиатурой, а выходные — с экраном, однако можно перенаправить эти потоки на другие устройства с помощью методов Set In и SetOut или средствами операционной системы (перенаправление с помощью операций < , > и » ).При обмене с консолью можно применять методы указанных потоков, но чаще используются методы класса Console — Read, ReadLine, Write и WriteLine, которые просто передают управление методам нижележащих классов In, Out и Error. Использование не одного, а двух выходных потоков полезно при желании разделить нормальный вывод программы и её сообщения об ошибках. Например, нормальный вывод программы можно перенаправить в файл, а сообщения об ошибках — на консоль или в файл журнала.

Работа с каталогами и файлами

В пространстве имен System. 10 есть четыре класса, предназначенные для работы с физическими файлами и структурой каталогов на диске: Directory, File, Directorylnfo и Filelnfo. С их помощью можно выполнять создание, удаление,перемещение файлов и каталогов, а также получение их свойств. Классы Directory и File реализуют свои функции через статические методы. Directorylnfo и Filelnfo обладают схожими возможностями, но они реализуются путем создания объектов соответствующих классов. Классы Directorylnfo и Filelnfo происходят от абстрактного класса FileSystemlnfo, который снабжает их базовыми свойствами.

Сохранение объектов (сериализация )

В С# есть возможность сохранять на внешних носителях не только данные примитивных типов, но и объекты. Сохранение объектов называется сериализацией, а восстановление сохраненных объектов — десериализацией. При сериализации объект преобразуется в линейную последовательность байтов. Это сложный процесс, поскольку объект может включать множество унаследованных полей и ссылки на вложенные объекты, которые, в свою очередь, тоже могут состоять из объектов сложной структуры. К счастью, сериализация выполняется автоматически, достаточно просто пометить класс как сериализуемый с помощью атрибута [Serial izable] . Атрибуты рассматриваются в главе 12, пока же достаточно знать, что атрибуты — это дополнительные сведения о классе, которые сохраняются в его метаданных. Те поля, которые сохранять не требуется, помечаются атрибутом [NonSerialized], например: [Serializable]

class Demo

{ public int a = 1:

[NonSerialized]

public double y:

public Monster X. Y; }

Объекты можно сохранять в одном из двух форматов: двоичном или SOA P (в виде XML-файла) . В первом случае следует подключить к программе пространство имен System.Runtime.Serialization.Formatters.Binary, во втором — пространство System.Runtime.Seri alizati on.Formatters.Soap.

Рассмотрим сохранение объектов в двоичном формате. Для этого используется класс BinaryFormatter, в котором определены два метода:

Serialize( поток, объект );

DeserializeC поток ):

Метод Serialize сохраняет заданный объект в заданном потоке, метод Deserialize восстанавливает объект из заданного потока.