Организация С#-системы ввода-вывода
С#-программы выполняют операции ввода-вывода посредством потоков, которые построены на иерархии классов. Поток (stream) - это абстракция, которая генерирует и принимает данные. С помощью потока можно читать данные из различных источников (клавиатура, файл) и записывать в различные источники (принтер, экран, файл). Несмотря на то, что потоки связываются с различными физическими устройствами, характер поведения всех потоков одинаков. Поэтому классы и методы ввода-вывода можно применить ко многим типам устройств.
На самом низком уровне иерархии потоков ввода-вывода находятся потоки, оперирующие байтами. Это объясняется тем, что многие устройства при выполнении операций ввода-вывода ориентированы на байты. Однако для человека привычнее оперировать символами, поэтому разработаны символьные потоки, которые фактически представляют собой оболочки, выполняющие преобразование байтовых потоков в символьные и наоборот. Кроме этого, реализованы потоки для работы с int -, double -, short - значениями, которые также представляют оболочку для байтовых потоков, но работают не с самими значениями, а с их внутренним представлением в виде двоичных кодов.
Центральную часть потоковой С#-системы занимает класс Stream пространства имен System.IO. Класс Stream представляет байтовый поток и является базовым для всех остальных потоковых классов. Из класса Stream выведены такие байтовые классы потоков как:
- FileStream - байтовый поток, разработанный для файлового ввода-вывода
- BufferedStream - заключает в оболочку байтовый поток и добавляет буферизацию, которая во многих случаях увеличивает производительность программы;
- MemoryStream - байтовый поток, который использует память для хранения данных.
Программист может вывести собственные потоковые классы. Однако для подавляющего большинства приложений достаточно встроенных потоков.
Подробно мы рассмотрим класс FileStream, классы StreamWriter и StreamReader, представляющие собой оболочки для класса FileStream и позволяющие преобразовывать байтовые потоки в символьные, а также классы BinaryWriter и BinaryReader, представляющие собой оболочки для класса FileStream и позволяющие преобразовывать байтовые потоки в двоичные для работы с int -, double -, short - и т.д. значениями.
Байтовый поток
Чтобы создать байтовый поток, связанный с файлом, создается объект класса FileStream. При этом в классе определено несколько конструкторов. Чаще всего используется конструктор, который открывает поток для чтения и/или записи:
FileStream(string filename, FileMode mode)
где:
- параметр filename определяет имя файла, с которым будет связан поток ввода-вывода данных; при этом filename определяет либо полный путь к файлу, либо имя файла, который находится в папке bin/debug вашего проекта.
- параметр mode определяет режим открытия файла, который может принимать одно из возможных значений, определенных перечислением FileMode:
- FileMode.Append - предназначен для добавления данных в конец файла;
- FileMode.Create - предназначен для создания нового файла, при этом если существует файл с таким же именем, то он будет предварительно удален;
- FileMode.CreateNew - предназначен для создания нового файла, при этом файл с таким же именем не должен существовать;
- FileMоde.Open - предназначен для открытия существующего файла;
- FileMode.ОpenOrCreate - если файл существует, то открывает его, в противном случае создает новый
- FileMode.Truncate - открывает существующий файл, но усекает его длину до нуля
Если попытка открыть файл оказалась неуспешной, то генерируется одно из исключений: FileNotFoundException - файл невозможно открыть по причине его отсутствия, IOException - файл невозможно открыть из-за ошибки ввода-вывода, ArgumentNullException - имя файла представляет собой null -значение, ArgumentException - некорректен параметр mode, SecurityException - пользователь не обладает правами доступа, DirectoryNotFoundException - некорректно задан каталог.
Другая версия конструктора позволяет ограничить доступ только чтением или только записью:
FileStream(string filename, FileMode mode, FileAccess how)
где:
- параметры filename и mode имеют то же назначение, что и в предыдущей версии конструктора;
- параметр how, определяет способ доступа к файлу и может принимать одно из значений, определенных перечислением FileAccess:
- FileAccess.Read - только чтение;
- FileAccess.Write - только запись;
- FileAccess.ReadWrite - и чтение, и запись.
После установления связи байтового потока с физическим файлом внутренний указатель потока устанавливается на начальный байт файла.
Для чтения очередного байта из потока, связанного с физическим файлом, используется метод ReadByte(). После прочтения очередного байта внутренний указатель перемещается на следующий байт файла. Если достигнут конец файла, то метод ReadByte() возвращает значение -1.
Для побайтовой записи данных в поток используется метод WriteByte().
По завершении работы с файлом его необходимо закрыть. Для этого достаточно вызвать метод Close (). При закрытии файла освобождаются системные ресурсы, ранее выделенные для этого файла, что дает возможность использовать их для работы с другими файлами.
Рассмотрим пример использования класса FileStream, для копирования одного файла в другой. Но вначале создадим текстовый файл text.txt в папке bin/debug текущего проекта. И внесем в него произвольную информацию, например:
12 456 Hello! 23,67 4: Message using System; using System.Text; using System.IO; //для работы с потоками namespace MyProgram { class Program { static void Main() { try { FileStream fileIn = new FileStream("text.txt", FileMode.Open, FileAccess.Read); FileStream fileOut = new FileStream("newText.txt", FileMode.Create, FileAccess.Write); int i; while ((i = fileIn.ReadByte())!=-1) { //запись очередного файла в поток, связанный с файлом fIleOut fileOut.WriteByte((byte)i); } fileIn.Close(); fileOut.Close(); } catch (Exception EX) { Console.WriteLine(EX.Message); } } } }
Символьный поток
Чтобы создать символьный поток нужно поместить объект класса Stream (например, FileStream ) "внутрь" объекта класса StreamWriter или объекта класса StreamReader. В этом случае байтовый поток будет автоматически преобразовываться в символьный.
Класс StreamWriter предназначен для организации выходного символьного потока. В нем определено несколько конструкторов. Один из них записывается следующим образом:
StreamWriter(Stream stream);
где параметр stream определяет имя уже открытого байтового потока.
Например, создать экземпляр класса StreamReader можно следующим образом:
StreamWriter fileOut=new StreamWriter(new FileStream("text.txt", FileMode.Create, FileAccess.Write));
Этот конструктор генерирует исключение типа ArgumentException, если поток stream не открыт для вывода, и исключение типа ArgumentNullException, если он (поток) имеет null-значение.
Другой вид конструктора позволяет открыть поток сразу через обращения к файлу:
StreamWriter(string name);
где параметр name определяет имя открываемого файла.
Например, обратиться к данному конструктору можно следующим образом:
StreamWriter fileOut=new StreamWriter("c:\temp\t.txt");
И еще один вариант конструктора StreamWriter:
StreamWriter(string name, bool appendFlag);
где параметр name определяет имя открываемого файла;
параметр appendFlag может принимать значение true - если нужно добавлять данные в конец файла, или false - если файл необходимо перезаписать.
Например:
StreamWriter fileOut=new StreamWriter("t.txt", true);
Теперь для записи данных в поток fileOut можно обратиться к методу WriteLine. Это можно сделать следующим образом:
fileOut.WriteLine("test");
В данном случае в конец файла t.txt будет дописано слово test.
Класс StreamReader предназначен для организации входного символьного потока. Один из его конструкторов выглядит следующим образом:
StreamReader(Stream stream);
где параметр stream определяет имя уже открытого байтового потока.
Этот конструктор генерирует исключение типа ArgumentException, если поток stream не открыт для ввода.
Например, создать экземпляр класса StreamWriter можно следующим образом:
StreamReader fileIn = new StreamReader(new FileStream("text.txt", FileMode.Open, FileAccess.Read));
Как и в случае с классом StreamWriter у класса StreamReader есть и другой вид конструктора, который позволяет открыть файл напрямую:
StreamReader (string name);
где параметр name определяет имя открываемого файла.
Обратиться к данному конструктору можно следующим образом:
StreamReader fileIn=new StreamReader ("c:\temp\t.txt");
В C# символы реализуются кодировкой Unicode. Для того, чтобы можно было обрабатывать текстовые файлы, содержащие русские символы, созданные, например, в Блокноте, рекомендуется вызывать следующий вид конструктора StreamReader:
StreamReader fileIn=new StreamReader ("c:\temp\t.txt", Encoding.GetEncoding(1251));
Параметр Encoding.GetEncoding(1251) говорит о том, что будет выполняться преобразование из кода Windows-1251 (одна из модификаций кода ASCII, содержащая русские символы) в Unicode. Encoding.GetEncoding(1251) реализован в пространстве имен System.Text.
Теперь для чтения данных из потока fileIn можно воспользоваться методом ReadLine. При этом если будет достигнут конец файла, то метод ReadLine вернет значение null.
Рассмотрим пример, в котором данные из одного файла копируются в другой, но уже с использованием классов StreamWriter и StreamReader.
static void Main() { StreamReader fileIn = new StreamReader("text.txt", Encoding.GetEncoding(1251)); StreamWriter fileOut=new StreamWriter("newText.txt", false); string line; while ((line=fileIn.ReadLine())!=null) //пока поток не пуст { fileOut.WriteLine(line); } fileIn.Close(); fileOut.Close(); }
Таким образом, данный способ копирования одного файла в другой, даст нам тот же результат, что и при использовании байтовых потоков. Однако, его работа будет менее эффективной, т.к. будет тратиться дополнительное время на преобразование байтов в символы. Но у символьных потоков есть свои преимущества. Например, мы можем использовать регулярные выражения для поиска заданных фрагментов текста в файле.
static void Main() { StreamReader fileIn = new StreamReader("text.txt"); StreamWriter fileOut=new StreamWriter("newText.txt", false); string text=fileIn.ReadToEnd(); Regex r= new Regex(@"[-+]?\d+"); Match integer = r.Match(text); while (integer.Success) { fileOut.WriteLine(integer); integer = integer.NextMatch(); } fileIn.Close(); fileOut.Close(); }