Опубликован: 19.02.2009 | Доступ: свободный | Студентов: 3054 / 785 | Оценка: 4.35 / 4.11 | Длительность: 16:28:00
ISBN: 978-5-94774-401-9
Лекция 11:

Организация С#-системы ввода-вывода

< Лекция 10 || Лекция 11: 123 || Лекция 12 >
Аннотация: Материалы данной лекции посвящены вопросам организации ввода-вывода C#. Рассмотрен байтовый, символьный и двоичный потоки.

С#-программы выполняют операции ввода-вывода посредством потоков, которые построены на иерархии классов. Поток (stream) - это абстракция, которая генерирует и принимает данные. С помощью потока можно читать данные из различных источников (клавиатура, файл) и записывать в различные источники (принтер, экран, файл). Несмотря на то, что потоки связываются с различными физическими устройствами, характер поведения всех потоков одинаков. Поэтому классы и методы ввода-вывода можно применить ко многим типам устройств.

На самом низком уровне иерархии потоков ввода-вывода находятся потоки, оперирующие байтами. Это объясняется тем, что многие устройства при выполнении операций ввода-вывода ориентированы на байты. Однако для человека привычнее оперировать символами, поэтому разработаны символьные потоки, которые фактически представляют собой оболочки, выполняющие преобразование байтовых потоков в символьные и наоборот. Кроме этого, реализованы потоки для работы с int -, double -, short - значениями, которые также представляют оболочку для байтовых потоков, но работают не с самими значениями, а с их внутренним представлением в виде двоичных кодов.

Центральную часть потоковой С#-системы занимает класс Stream пространства имен System.IO. Класс Stream представляет байтовый поток и является базовым для всех остальных потоковых классов. Из класса Stream выведены такие байтовые классы потоков как:

  1. FileStream - байтовый поток, разработанный для файлового ввода-вывода
  2. BufferedStream - заключает в оболочку байтовый поток и добавляет буферизацию, которая во многих случаях увеличивает производительность программы;
  3. MemoryStream - байтовый поток, который использует память для хранения данных.

Программист может вывести собственные потоковые классы. Однако для подавляющего большинства приложений достаточно встроенных потоков.

Подробно мы рассмотрим класс FileStream, классы StreamWriter и StreamReader, представляющие собой оболочки для класса FileStream и позволяющие преобразовывать байтовые потоки в символьные, а также классы BinaryWriter и BinaryReader, представляющие собой оболочки для класса FileStream и позволяющие преобразовывать байтовые потоки в двоичные для работы с int -, double -, short - и т.д. значениями.

Байтовый поток

Чтобы создать байтовый поток, связанный с файлом, создается объект класса FileStream. При этом в классе определено несколько конструкторов. Чаще всего используется конструктор, который открывает поток для чтения и/или записи:

FileStream(string filename, FileMode mode)

где:

  1. параметр filename определяет имя файла, с которым будет связан поток ввода-вывода данных; при этом filename определяет либо полный путь к файлу, либо имя файла, который находится в папке bin/debug вашего проекта.
  2. параметр 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)

где:

  1. параметры filename и mode имеют то же назначение, что и в предыдущей версии конструктора;
  2. параметр how, определяет способ доступа к файлу и может принимать одно из значений, определенных перечислением FileAccess:
  1. FileAccess.Read - только чтение;
  2. FileAccess.Write - только запись;
  3. 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);
   }
  }
 }
}
Задание. Подумайте, почему для переменной i указан тип int. Можно было бы указать тип byte?

Символьный поток

Чтобы создать символьный поток нужно поместить объект класса 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();
}
Задание. Выясните, для чего предназначен метод ReadToEnd() и когда имеется смысл его применять.

Таким образом, данный способ копирования одного файла в другой, даст нам тот же результат, что и при использовании байтовых потоков. Однако, его работа будет менее эффективной, т.к. будет тратиться дополнительное время на преобразование байтов в символы. Но у символьных потоков есть свои преимущества. Например, мы можем использовать регулярные выражения для поиска заданных фрагментов текста в файле.

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();
}
< Лекция 10 || Лекция 11: 123 || Лекция 12 >