Книга: C# 4.0: полное руководство

Класс FileStream и байтовый ввод-вывод в файл

Класс FileStream и байтовый ввод-вывод в файл

В среде .NET Framework предусмотрены классы для организации ввода-вывода в файлы. Безусловно, это в основном файлы дискового типа. На уровне операционной системы файлы имеют байтовую организацию. И, как следовало ожидать, для ввода и вывода байтов в файлы имеются соответствующие методы. Поэтому ввод и вывод в файлы байтовыми потоками весьма распространен. Кроме того, байтовый поток ввода или вывода в файл может быть заключен в соответствующий объект символьного потока. Операции символьного ввода-вывода в файл находят применение при обработке текста. О символьных потоках речь пойдет далее в этой главе, а здесь рассматривается байтовый ввод-вывод.

Для создания байтового потока, привязанного к файлу, служит класс FileStream. Этот класс является производным от класса Stream и наследует всего его функции.

Напомним, что классы потоков, в том числе и FileStream, определены в пространстве имен System.IO. Поэтому в самом начале любой использующей их программы обычно вводится следующая строка кода.

using System.IO;

Открытие и закрытие файла

Для формирования байтового потока, привязанного к файлу, создается объект класса FileStream. В этом классе определено несколько конструкторов. Ниже приведен едва ли не самый распространенный среди них:

FileStream(string путь, FileMode режим)

где путь обозначает имя открываемого файла, включая полный путь к нему; а режим — порядок открытия файла. В последнем случае указывается одно из значений, определяемых в перечислении FileMode и приведенных в табл. 14.4. Как правило, этот конструктор открывает файл для доступа с целью чтения или записи. Исключением из этого правила служит открытие файла в режиме FileMode.Append, когда файл становится доступным только для записи.

Таблица 14.4. Значения из перечисления FileMode

FileMode.Append - Добавляет выводимые данные в конец файла

FileMode.Create - Создает новый выходной файл. Существующий файл с таким же именем будет разрушен

FileMode.CreateNew -  Создает новый выходной файл. Файл с таким же именем не должен существовать

FileMode.Open - Открывает существующий файл

FileMode.OpenOrCreate - Открывает файл, если он существует. В противном случае создает новый файл

FileMode.Truncate - Открывает существующий файл, но сокращает его длину до нуля

Если попытка открыть файл оказывается неудачной, то генерируется исключение. Если же файл нельзя открыть из-за того что он не существует, генерируется исключение FileNotFoundException. А если файл нельзя открыть из-за какой-нибудь ошибки ввода-вывода, то генерируется исключение IOException. К числу других исключений, которые могут быть сгенерированы при открытии файла, относятся следующие: ArgumentNullException (указано пустое имя файла), ArgumentException (указано неверное имя файла), ArgumentOutOfRangeException (указан неверный режим), SecurityException (у пользователя нет прав доступа к файлу), PathTooLongException (слишком длинное имя файла или путь к нему), NotSupportedException (в имени файла указано устройство, которое не поддерживается), а также DirectoryNotFoundException (указан неверный каталог).

Исключения PathTooLongException, DirectoryNotFoundException и FileNotFoundException относятся к подклассам класса исключений IOException. Поэтому все они могут быть перехвачены, если перехватывается исключение IOException.

Ниже в качестве примера приведен один из способов открытия файла test.dat для ввода.

FileStream fin;
try {
fin = new FileStream("test", FileMode.Open);
}
catch(IOException exc) { // перехватить все исключения, связанные с вводом-выводом
  Console.WriteLine(exc.Message);
  // Обработать ошибку.
}
catch(Exception exc { // перехватить любое другое исключение.
  Console.WriteLine(exc.Message);
  // Обработать ошибку, если это возможно.
  // Еще раз сгенерировать необрабатываемые исключения.
}

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

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

Как упоминалось выше, конструктор класса FileStream открывает файл, доступный для чтения или записи. Если же требуется ограничить доступ к файлу только для чтения или же только для записи, то в таком случае следует использовать такой конструктор.

FileStream(string путь, FileMode режим, FileAccess доступ)

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

FileAccess.Read
FileAccess.Write
FileAccess.ReadWrite

Например, в следующем примере кода файл test.dat открывается только для чтения.

FileStream fin = new FileStream("test.dat", FileMode.Open, FileAccess.Read);

По завершении работы с файлом его следует закрыть, вызвав метод Close(). Ниже приведена общая форма обращения к этому методу.

void Close()

При закрытии файла высвобождаются системные ресурсы, распределенные для этого файла, что дает возможность использовать их для другого файла. Любопытно, что метод Close() вызывает, в свою очередь, метод Dispose(), который, собственно, и высвобождает системные ресурсы.

---------------------------------------

ПРИМЕЧАНИЕ

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

----------------------------------------

Чтение байтов из потока файлового ввода-вывода

В классе FileStream определены два метода для чтения байтов из файла: ReadByte() и Read(). Так, для чтения одного байта из файла используется метод ReadByte(), общая форма которого приведена ниже.

int ReadByte()

Всякий раз, когда этот метод вызывается, из файла считывается один байт, который затем возвращается в виде целого значения. К числу вероятных исключений, которые генерируются при этом, относятся NotSupportedException (поток не открыт для ввода) и ObjectDisposedException (поток закрыт).

Для чтения блока байтов из файла служит метод Read(), общая форма которого выглядит так.

int Read(byte[ ] array, int offset, int count)

В методе Read() предпринимается попытка считать количество count байтов в массив array, начиная с элемента array[offset]. Он возвращает количество байтов, успешно считанных из файла. Если же возникает ошибка ввода-вывода, то генерируется исключение IOException. К числу других вероятных исключений, которые генерируются при этом, относится NotSupportedException. Это исключение генерируется в том случае, если чтение из файла не поддерживается в потоке.

В приведенном ниже примере программы метод ReadByte() используется для ввода и отображения содержимого текстового файла, имя которого указывается в качестве аргумента командной строки. Обратите внимание на то, что в этой программе проверяется, указано ли имя файла, прежде чем пытаться открыть его.

/* Отобразить содержимое текстового файла.
Чтобы воспользоваться этой программой, укажите имя того файла, содержимое которого требуется отобразить. Например, для просмотра содержимого файла TEST.CS введите в командной строке следующее:
ShowFile TEST.CS
*/
using System;
using System.IO;
class ShowFile {
  static void Main(string[] args) {
    int i;
    FileStream fin;
    if(args.Length != 1) {
      Console.WriteLine("Применение: ShowFile Файл");
      return;
    }
    try {
      fin = new FileStream(args[0], FileMode.Open);
    } catch(IOException exc) {
      Console.WriteLine("He удается открыть файл");
      Console.WriteLine(exc.Message);
      return; // Файл не открывается, завершить программу
    }
    // Читать байты до конца файла,
    try {
      do {
        i = fin.ReadByte();
        if(i != -1) Console.Write((char) i);
      } while(i != -1);
    } catch(IOException exc) {
      Console.WriteLine("Ошибка чтения файла");
      Console.WriteLine(exc.Message) ;
    } finally {
      fin.Close() ;
    }
  }
}

Обратите внимание на то, что в приведенной выше программе применяются два блока try. В первом из них перехватываются исключения, возникающие при вводе-выводе и способные воспрепятствовать открытию файла. Если произойдет ошибка ввода-вывода, выполнение программы завершится. В противном случае во втором блоке try будет продолжен контроль исключений, возникающих в операциях ввода-вывода. Следовательно, второй блок try выполняется только в том случае, если в переменной fin содержится ссылка на открытый файл. Обратите также внимание на то, что файл закрывается в блоке finally, связанном со вторым блоком try. Это означает, что независимо от того, как завершится цикл do-while (нормально или аварийно из-за ошибки), файл все равно будет закрыт. И хотя в данном конкретном примере это и так важно, поскольку программа все равно завершится в данной точке, преимущество такого подхода, вообще говоря, заключается в том, что файл закрывается в завершающем блоке finally в любом случае — даже если выполнение кода доступа к этому файлу завершается преждевременно из-за какого-нибудь исключения.

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

// Отобразить содержимое текстового файла.
using System;
using System.IO;
class ShowFile {
  static void Main(string[] args) {
    int i;
    FileStream fin = null;
    if (args.Length != 1) {
      Console.WriteLine("Применение: ShowFile File");
      return;
    }
    try {
      fin = new FileStream(args[0], FileMode.Open);
      // Читать байты до конца файла,
      do {
        i = fin.ReadByte();
        if(i != -1) Console.Write((char) i);
      } while (i != -1);
    } catch(IOException exc) {
      Console.WriteLine("Ошибка ввода-вывода:n" + exc.Message);
    } finally {
      if(fin != null) fin.Close();
    }
  }
}

Обратите внимание на то, что в данном варианте программы переменная fin ссылки на объект класса FileStream инициализируется пустым значением. Если файл удастся открыть в конструкторе класса FileStream, то значение переменной fin окажется непустым, а иначе — оно так и останется пустым. Это очень важно, поскольку метод Close() вызывается внутри блока finally только в том случае, если значение переменной fin оказывается непустым. Подобный механизм препятствует любой попытке вызвать метод Сlose() для переменной fin, когда она не ссылается на открытый файл. Благодаря своей компактности такой подход часто применяется во многих примерах организации ввода-вывода, приведенных далее в этой книге. Следует, однако, иметь в виду, что он не пригоден в тех случаях, когда ситуацию, возникающую в связи с невозможностью открыть файл, нужно обрабатывать отдельно. Так, если пользователь неправильно введет имя файла, то на экран, возможно, придется вывести приглашение правильно ввести имя файла, прежде чем входить в блок try, где осуществляется проверка правильности доступа к файлу.

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

Запись в файл

Для записи байта в файл служит метод WriteByte(). Ниже приведена его простейшая форма.

void WriteByte(byte value)

Этот метод выполняет запись в файл байта, обозначаемого параметром value. Если базовый поток не открывается для вывода, то генерируется исключение NotSupportedException. А если поток закрыт, то генерируется исключение ObjectDisposedException.

Для записи в файл целого массива байтов может быть вызван метод Write(). Ниже приведена его общая форма.

void Write(byte[] array, int offset, int count)

В методе Write() предпринимается попытка записать в файл количество count байтов из массива array, начиная с элемента array[offset]. Он возвращает количество байтов, успешно записанных в файл. Если во время записи возникает ошибка, то генерируется исключение IOException. А если базовый поток не открывается для вывода, то генерируется исключение NotSupportedException. Кроме того, может быть сгенерирован ряд других исключений.

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

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

void Flush()

При неудачном исходе данной операции генерируется исключение IOException. Если же поток закрыт, то генерируется исключение ObjectDisposedException.

По завершении вывода в файл следует закрыть его с помощью метода Close(). Этим гарантируется, что любые выведенные данные, оставшиеся в дисковом буфере, будут записаны на диск. В этом случае отпадает необходимость вызывать метод Flush() перед закрытием файла.

Ниже приведен простой пример программы, в котором демонстрируется порядок записи данных в файл.

// Записать данные в файл.
using System;
using System.IO;
class WriteToFile {
  static void Main(string[] args) {
    FileStream fout = null;
    try {
      // Открыть выходной файл.
      fout = new FileStream("test.txt", FileMode.CreateNew);
      // Записать весь английский алфавит в файл,
      for(char c = 'А'; c <= 'Z'; c++) fout.WriteByte((byte) c);
    } catch(IOException exc) {
      Console.WriteLine ("Ошибка ввода-вывода: n" + exc.Message) ;
    } finally {
      if(fout != null) fout.Close();
    }
  }
}

В данной программе сначала создается выходной файл под названием test.txt с помощью перечисляемого значения FileMode.CreateNew. Это означает, что файл с таким же именем не должен уже существовать. (В противном случае генерируется исключение IOException.) После открытия выходного файла в него записываются

прописные буквы английского алфавита. По завершении данной программы содержимое файла test.txt оказывается следующим.

ABCDEFGHIJKLMNOPQRSTUVWXYZ

Использование класса FileStream для копирования файла

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

/* Копировать файл.
Чтобы воспользоваться этой программой, укажите имена исходного и выходного файлов. Например, для копирования файла FIRST.DAT в файл SECOND.DAT введите в командной строке следующее:
CopyFile FIRST.DAT SECOND.DAT
*/
using System;
using System.IO;
class CopyFile {
  static void Main(string[] args) {
    int i;
    FileStream fin = null;
    FileStream fout = null;
    if (args.Length != 2) {
      Console.WriteLine("Применение: CopyFile Откуда Куда");
      return;
    }
    try {
      // Открыть файлы.
      fin = new FileStream(args[0], FileMode.Open);
      fout = new FileStream(args[1], FileMode.Create);
      // Скопировать файл,
      do {
        i = fin.ReadByte();
        if (i != -1) fout.WriteByte((byte)i);
      } while (i != -1);
    }
    catch (IOException exc) {
      Console.WriteLine("Ошибка ввода-вывода:n" + exc.Message);
    }
    finally {
      if (fin != null) fin.Close();
      if (fout != null) fout.Close();
    }
  }
}

Оглавление книги


Генерация: 0.062. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз