В примерах программ, приводившихся в предыду щих главах, уже применялись отдельные части си стемы ввода-вывода в С#, например метод Console. WriteLine(), но делалось это без каких-либо формальных пояснений. Система ввода-вывода основана в C# на иерар хии классов, поэтому ее функции и особенности нельзя было представлять до тех пор, пока не были рассмотрены классы, наследование и исключения. А теперь настал черед и для ввода-вывода. В C# применяется система ввода-вывода и классы, определенные в среде .NET Framework, и поэтому рассмотрение ввода-вывода в этом языке относится ко всей системе ввода-вывода среды .NET в целом.
В этой главе речь пойдет о средствах консольного и фай лового ввода-вывода. Следует, однако, сразу же предупре дить, что система ввода-вывода в C# довольно обширна. По этому в этой главе рассматриваются лишь самые важные и наиболее часто используемые ее средства.
Ввод-вывод в программах на C# осуществляется посред ством потоков. Поток — это некая абстракция производ ства или потребления информации. С физическим устрой ством поток связывает система ввода-вывода. Все потоки действуют одинаково — даже если они связаны с разными физическими устройствами. Поэтому классы и методы ввода-вывода могут применяться к самым разным типам устройств. Например, методами вывода на консоль можно пользоваться и для вывода в файл на диске.
На самом низком уровне ввод-вывод в С# осуществляется байтами. И делается это потому, что многие устройства ориентированы на операции ввода-вывода отдельными байтами. Но человеку больше свойственно общаться символами. Напомним, что в C# тип char является 16-разрядным, а тип byte — 8-разрядным. Так, если в целях ввода- вывода используется набор символов в коде ASCII, то для преобразования типа char в тип byte достаточно отбросить старший байт значения типа char. Но это не годится для набора символов в уникоде (Unicode), где символы требуется представлять двумя, а то и больше байтами. Следовательно, байтовые потоки не совсем подходят для ор ганизации ввода-вывода отдельными символами. С целью разрешить это затруднение в среде .NET Framework определено несколько классов, выполняющих превращение байтового потока в символьный с автоматическим преобразованием типа byte в тип char и обратно.
Для всех программ, в которых используется пространство имен System, доступны встроенные потоки, открывающиеся с помощью свойств Console.In, Console.Out и Console.Error. В частности, свойство Console.Out связано со стандартным по током вывода. По умолчанию это поток вывода на консоль. Так, если вызывается метод Console.WriteLine(), информация автоматически передается свойству Console. Out. Свойство Console.In связано со стандартным потоком ввода, который по умол чанию осуществляется с клавиатуры. А свойство Console.Error связано со стан дартным потоком сообщений об ошибках, которые по умолчанию также выводятся на консоль. Но эти потоки могут быть переадресованы на любое другое совместимое устройство ввода-вывода. Стандартные потоки являются символьными. Поэтому в эти потоки выводятся и вводятся из них символы.
В среде .NET Framework определены классы как для байтовых, так и для символь ных потоков. Но на самом деле классы символьных потоков служат лишь оболочками для превращения заключенного в них байтового потока в символьный, автоматически выполняя любые требующиеся преобразования типов данных. Следовательно, сим вольные потоки основываются на байтовых, хотя они и разделены логически. Основные классы потоков определены в пространстве имен System.IO. Для того чтобы воспользоваться этими классами, как правило, достаточно ввести приведенный ниже оператор в самом начале программы.
using System.IO;
Пространство имен System.IO не указывается для консольного ввода-вывода по тому, что для него определен класс Console в пространстве имен System.
Основным для потоков является класс System.IO.Stream. Он представляет бай товый поток и является базовым для всех остальных классов потоков. Кроме того, он является абстрактным классом, а это означает, что получить экземпляр объекта класса Stream нельзя. В классе Stream определяется ряд операций со стандартными пото ками, представленных соответствующими методами. В табл. 14.1 перечислен ряд наи более часто используемых методов, определенных в классе Stream.
Таблица 14.1. Некоторые методы, определенные в классе Stream
Метод | Описание |
---|---|
void Close() | Закрывает поток |
void Flush() | Выводит содержимое потока на физическое устройство |
int ReadByte() | Возвращает целочисленное представление следующего байта, доступного для ввода из потока. При обнаружении конца файла возвращает значение -1 |
int Read(byte[] buffer, int offset, int count) | Делает попытку ввести count байтов в массив buffer, начиная с элемента buffer[offset]. Возвращает количество успешно введенных байтов |
long Seek(long offset, SeekOrigin origin) | Устанавливает текущее положение в потоке по указанному смещению offset относительно заданного начала отсчета origin. Возвращает новое положение в потоке |
void WriteByte(byte value) | Выводит один байт в поток вывода |
void Write(byte[]buffer, int offset, int count) | Выводит подмножество count байтов из массива buffer, начиная с элемента buffer[offset]. Возвращает количество выведенных байтов |
Некоторые из методов, перечисленных в табл. 14.1, генерируют исключение IOException при появлении ошибки ввода-вывода. Если же предпринимается по пытка выполнить неверную операцию, например вывести данные в поток, предназна ченный только для чтения, то генерируется исключение NotSupportedException. Кроме того, могут быть сгенерированы и другие исключения — все зависит от кон кретного метода.
Следует заметить, что в классе Stream определены методы для ввода (или чтения) и вывода (или записи) данных. Но не все потоки поддерживают обе эти операции, поскольку поток можно открывать только для чтения или только для записи. Кроме того, не все потоки поддерживают запрос текущего положения в потоке с помощью метода Seek(). Для того чтобы определить возможности потока, придется воспользо ваться одним, а то и несколькими свойствами класса Stream. Эти свойства перечисле ны в табл. 14.2 наряду со свойствами Length и Position, содержащими длину потока и текущее положение в нем.
Таблица 14.2. Свойства, определенные в классе Stream
Свойство | Описание |
---|---|
bool CanRead | Принимает значение true, если из потока можно ввести данные. Доступно только для чтения |
bool CanSeek | Принимает значение true, если поток поддерживает запрос текущего положения в потоке. Доступно только для чтения |
bool CanWrite | Принимает значение true, если в поток можно вывести данные. Доступно только для чтения |
long Length | Содержит длину потока. Доступно только для чтения |
long Position | Представляет текущее положение в потоке. Доступно как для чтения, так и для записи |
int ReadTimeout | Представляет продолжительность времени ожидания в операциях ввода. Доступно как для чтения, так и для записи |
int WriteTimeout | Представляет продолжительность времени ожидания в операциях вывода. Доступно как для чтения, так и для записи |
Производными от класса Stream являются несколько конкретных классов байтовых потоков. Эти классы определены в пространстве имен System.IO и перечислены ниже.
Класс потока | Описание |
---|---|
BufferedStream | Заключает в оболочку байтовый поток и добавляет буферизацию. Буферизация, как правило, повышает производительность |
FileStream | Байтовый поток, предназначенный для файлового ввода-вывода |
MemoryStream | Байтовый поток, использующий память для хранения данных |
UnmanagedMemoryStream | Байтовый поток, использующий неуправляемую память для хранения данных |
В среде NET Framework поддерживается также ряд других конкретных классов по токов, в том числе для ввода-вывода в сжатые файлы, сокеты и каналы. Кроме того, можно создать свои собственные производные классы потоков, хотя для подавляющего числа приложений достаточно и встроенных потоков.
Для создания символьного потока достаточно заключить байтовый поток в один из классов-оболочек символьных потоков. На вершине иерархии классов символь ных потоков находятся абстрактные классы TextReader и TextWriter. Так, класс TextReader организует ввод, а класс TextWriter — вывод. Методы, определенные в обоих этих классах, доступны для всех их подклассов. Они образуют минимальный набор функций ввода-вывода, которыми должны обладать все символьные потоки.
В табл. 14.3 перечислены методы ввода, определенные в классе TextReader. В це лом, эти методы способны генерировать исключение IOException при появлении ошибки ввода, а некоторые из них — исключения других типов. Особый интерес вы зывает метод ReadLine(), предназначенный для ввода целой текстовой строки, воз вращая ее в виде объекта типа string. Этот метод удобен для чтения входных данных, содержащих пробелы. В классе TextReader имеется также метод Close(), опреде ляемый следующим образом.
void Close()
Этот метод закрывает считывающий поток и освобождает его ресурсы.
Таблица 14.3. Методы ввода, определенные в классе TextReader
Метод | Описание |
---|---|
int Рeек() | Получает следующий символ из потока ввода, но не удаляет его. Возвращает значение -1, если ни один из символов не доступен |
int Read() | Возвращает целочисленное представление следующего доступного символа из вызывающего потока ввода. При обнаружении конца потока возвращает значение -1 |
int Read(char[]buffer, int index, int count) | Делает попытку ввести количество count символов в массив buffer, начиная с элемента buffer[index], и возвращает количество успешно введенных символов |
int ReadBlock(char[]buffer, int index, int count) | Делает попытку ввести количество count сим волов в массив buffer, начиная с элемента buffer[index], и возвращает количество успешно введенных символов |
string ReadLine() | Вводит следующую текстовую строку и возвращает ее в виде объекта типа string. При попытке прочитать признак конца файла возвращает пустое значение |
string ReadToEnd() | Вводит все символы, оставшиеся в потоке, и возвращает их в виде объекта типа string |
В классе TextWriter определены также варианты методов Write() и WriteLine(), предназначенные для вывода данных всех встроенных типов. Ниже в качестве при мера перечислены лишь некоторые из перегружаемых вариантов этих методов.
Метод | Описание |
---|---|
void Write(int value) | Выводит значение типа int |
void Write(double value) | Выводит значение типа double |
void Write(bool value) | Выводит значение типа bool |
void WriteLine(string value) | Выводит значение типа string с последующим символом новой строки |
void WriteLine(uint value) | Выводит значение типа uint с последующим символом новой строки |
void WriteLine(char value) | Выводит символ с последующим символом новой строки |
Все эти методы генерируют исключение IOException при появлении ошибки вывода.
Кроме того, в классе TextWriter определены методы Close() и Flush(), при веденные ниже.
virtual void Close()
virtual void Flush()
Метод Flush() организует вывод в физическую среду всех данных, оставшихся в выходном буфере. А метод Close() закрывает записывающий поток и освобождает его ресурсы.
Классы TextReader и TextWriter реализуются несколькими классами символь ных потоков, включая и те, что перечислены ниже. Следовательно, в этих классах потоков предоставляются методы и свойства, определенные в классах TextReader и TextWriter.
Класс потока | Описание |
---|---|
StreamReader | Предназначен для ввода символов из байтового потока. Этот класс является оболочкой для байтового потока ввода |
StreamWriter | Предназначен для вывода символов в байтовый поток. Этот класс является оболочкой для байтового потока вывода |
StringReader | Предназначен для ввода символов из символьной строки |
StringWriter | Предназначен для вывода символов в символьную строку |
Помимо классов байтовых и символьных потоков, имеются еще два класса двоич ных потоков, которые могут служить для непосредственного ввода и вывода двоичных данных — BinaryReader и BinaryWriter. Подробнее о них речь пойдет далее в этой главе, когда дойдет черед до файлового ввода-вывода.
А теперь, когда представлена общая структура системы ввода-вывода в С#, отведем оставшуюся часть этой главы более подробному рассмотрению различных частей дан ной системы, начиная с консольного ввода-вывода.
Консольный ввод-вывод осуществляется с помощью стандартных потоков, представ ленных свойствами Console.In, Console.Out и Console.Error. Примеры консольною ввода-вывода были представлены еще в главе 2, поэтому он должен быть вам уже знаком. Как будет показано ниже, он обладает и рядом других дополнительных возможностей.
Но прежде следует еще раз подчеркнуть, что большинство реальных приложений C# ориентированы не на консольный ввод-вывод в текстовом виде, а на графический оконный интерфейс для взаимодействия с пользователем, или же они представляют собой программный код, используемый на стороне сервера. Поэтому часть системы ввода-вывода, связанная с консолью, не находит широкого практического применения. И хотя программы, ориентированные на текстовый ввод-вывод, отлично подходят в качестве учебных примеров, коротких сервисных программ или определенного рода программных компонентов, для большинства реальных приложений они не годятся.
Поток Console.In является экземпляром объекта класса TextReader, и поэто му для доступа к нему могут быть использованы методы и свойства, определенные в классе TextReader. Но для этой цеди чаще все же используются методы, предостав ляемые классом Console, в котором автоматически организуется чтение данных из по тока Console.In. В классе Console определены три метода ввода. Два первых метода, Read() и ReadLine(), были доступны еще в версии .NET Framework 1.0. А третий метод, ReadKey(), был добавлен в версию 2.0 этой среды.
Для чтения одного символа служит приведенный ниже метод Read().
static int Read()
Метод Read() возвращает очередной символ, считанный с консоли. Он ожидает до тех пор, пока пользователь не нажмет клавишу, а затем возвращает результат. Воз вращаемый символ относится к типу int и поэтому должен быть приведен к типу char. Если при вводе возникает ошибка, то метод Read() возвращает значение -1. Этот метод сгенерирует исключение IOException при неудачном исходе операции ввода. Ввод с консоли с помощью метода Read() буферизуется построчно, поэтому пользователь должен нажать клавишу , прежде чем программа получит любой символ, введенный с консоли.
Ниже приведен пример программы, в которой метод Read() используется для считывания символа, введенного с клавиатуры.
// Считать символ, введенный с клавиатуры.
using System;
class KbIn {
static void Main() {
char ch;
Console.Write("Нажмите клавишу, а затем — <ENTER>: ");
ch = (char) Console.Read(); // получить значение типа char
Console.WriteLine("Вы нажали клавишу: " + ch);
}
}
Вот, например, к какому результату может привести выполнение этой программы.
Нажмите клавишу, а затем — <ENTER>: t
Вы нажали клавишу: t
Необходимость буферизировать построчно ввод, осуществляемый с консоли по средством метода Read(), иногда может быть досадным препятствием. Ведь при нажа тии клавиши в поток ввода передается последовательность символов перевода каретки и перевода строки. Более того, эти символы остаются во входном буфере до тех пор, пока они не будут считаны. Следовательно, в некоторых приложениях приходится удалять эти символы (путем их считывания), прежде чем приступать к следующей опе рации ввода. Впрочем, для чтения введенных с клавиатуры символов без построчной буферизации, можно воспользоваться рассматриваемым далее методом ReadKey().
Для считывания строки символов служит приведенный ниже метод ReadLine().
static string ReadLine()
Символы считываются методом ReadLine() до тех пор, пока пользователь не
нажмет клавишу <Enter>
, а затем этот метод возвращает введенные символы в виде
объекта типа string. Кроме того, он сгенерирует исключение IOException при неу
дачном исходе операции ввода.
Ниже приведен пример программы, в которой демонстрируется чтение строки из потока Console.In с помощью метода ReadLine().
// Ввод с консоли с помощью метода ReadLine().
using System;
class ReadString {
static void Main() {
string str;
Console.WriteLine("Введите несколько символов.");
str = Console.ReadLine();
Console.WriteLine("Вы ввели: " + str);
}
}
Выполнение этой программы может привести, например, к следующему результату.
Введите несколько символов.
Это просто тест.
Вы ввели: Это просто тест.
Итак, для чтения данных из потока Console.In проще всего воспользоваться мето дами класса Console. Но для этой цели можно обратиться и к методам базового клас са TextReader. В качестве примера ниже приведен переделанный вариант предыду щего примера программы, в котором используется метод Rea.dLine(), определенный в классе TextReader.
// Прочитать введенную с клавиатуры строку
// непосредственно из потока Console.In.
using System;
class ReadChars2 {
static void Main() {
string str;
Console.WriteLine("Введите несколько символов.");
str = Console.In.ReadLine(); // вызвать метод ReadLine() класса TextReader
Console.WriteLine("Вы ввели: " + str);
}
}
Обратите внимание на то, что метод ReadLine() теперь вызывается непосредствен но для потока Console.In. Поэтому если требуется доступ к методам, определенным в классе TextReader, который является базовым для потока Console.In, то подобные методы вызываются так, как было показано в приведенном выше примере.
В состав среды .NET Framework включен метод, определяемый в классе Console
и позволяющий непосредственно считывать отдельно введенные с клавиатуры
символы без построчной буферизации. Этот метод называется ReadKey(). При нажа
тии клавиши метод ReadKey() немедленно возвращает введенный с клавиатуры сим
вол. И в этом случае пользователю уже не нужно нажимать дополнительно клавишу
<Enter>
. Таким образом, метод ReadKey() позволяет считывать и обрабатывать ввод
с клавиатуры в реальном масштабе времени.
Ниже приведены две формы объявления метода ReadKey().
static ConsoleKeyInfo ReadKey()
static ConsoleKeyInfo ReadKey(bool intercept)
В первой форме данного метода ожидается нажатие клавиши. Когда оно проис ходит, метод возвращает введенный с клавиатуры символ и выводит его на экран. Во второй форме также ожидается нажатие клавиши, и затем возвращается введенный с клавиатуры символ. Но если значение параметра intercept равно true, то введен ный символ не отображается. А если значение параметра intercept равно false, то введенный символ отображается.
Метод ReadKey() возвращает информацию о нажатии клавиши в объекте типа ConsoleKeyInfo, который представляет собой структуру, состоящую из приведенных ниже свойств, доступных только для чтения.
char KeyChar
ConsoleKey Key
ConsoleModifiers Modifiers
Свойство KeyChar содержит эквивалент char введенного с клавиатуры символа, свойство Key — значение из перечисления ConsoleKey всех клавиш на клавиатуре, а свойство Modifiers — описание одной из модифицирующих клавиш (, или ), которые были нажаты, если это действительно имело место, при фор мировании ввода с клавиатуры. Эти модифицирующие клавиши представлены в перечислении ConsoleModifiers следующими значениями: Control, Shift и Alt. В свойстве Modifiers может присутствовать несколько значений нажатых модифи цирующих клавиш.
Главное преимущество метода ReadKey() заключается в том, что он предоставляет средства для организации ввода с клавиатуры в диалоговом режиме, поскольку этот ввод не буферизуется построчно. Для того чтобы продемонстрировать данный метод в действии, ниже приведен соответствующий пример программы.
// Считать символы, введенные с консоли, используя метод ReadKey().
using System;
class ReadKeys {
static void Main() {
ConsoleKeyInfo keypress;
Console.WriteLine("Введите несколько символов, " +
"а по окончании - <Q>.");
do {
keypress = Console.ReadKey(); // считать данные о нажатых клавишах
Console.WriteLine(" Вы нажали клавишу: " + keypress.KeyChar);
// Проверить нажатие модифицирующих клавиш.
if((ConsoleModifiers.Alt & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Alt>.");
if((ConsoleModifiers.Control & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Control>.");
if((ConsoleModifiers.Shift & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Shift>.");
} while(keypress.KeyChar != 'Q');
}
}
Вот, например, к какому результату может привести выполнение этой программы.
Введите несколько символов, а по окончании - <Q>.
а Вы нажали клавишу: а
b Вы нажали клавишу: b
d Вы нажали клавишу: d
А Вы нажали клавишу: А
Нажата клавиша <Shift>.
В Вы нажали клавишу: В
Нажата клавиша <Shift>.
С Вы нажали клавишу: С
Нажата клавиша <Shift>.
• Вы нажали клавишу: •
Нажата клавиша <Control>.
Q Вы нажали клавишу: Q
Нажата клавиша <Shift>.
Как следует из приведенного выше результата, всякий раз, когда нажимается клави ша, метод ReadKey() немедленно возвращает введенный с клавиатуры символ. Этим он отличается от упоминавшегося ранее метода Read(), в котором ввод выполняется с построчной буферизацией. Поэтому если требуется добиться в программе реакции на ввод с клавиатуры, то рекомендуется выбрать метод ReadKey().
Потоки Console.Out и Console.Error являются объектами типа TextWriter. Вывод на консоль проще всего осуществить с помощью методов Write() и WriteLine(), с которыми вы уже знакомы. Существуют варианты этих методов для вывода данных каждого из встроенных типов. В классе Console определяются его соб ственные варианты метода Write() и WriteLine(), и поэтому они могут вызываться непосредственно для класса Console, как это было уже не раз показано на страницах данной книги. Но при желании эти и другие методы могут быть вызваны и для класса TextWriter, который является базовым для потоков Console.Out и Console.Error.
Ниже приведен пример программы, в котором демонстрируется вывод в потоки Console.Out и Console.Error. По умолчанию данные в обоих случаях выводятся на консоль.
// Организовать вывод в потоки Console.Out и Console.Error.
using System;
class ErrOut {
static void Main() {
int a=10, b=0;
int result;
Console.Out.WriteLine("Деление на нуль приведет " +
"к исключительной ситуации.");
try {
result = a / b; // сгенерировать исключение при попытке деления на нуль
} catch(DivideByZeroException exc) {
Console.Error.WriteLine(exc.Message);
}
}
}
При выполнении этой программы получается следующий результат.
Деление на нуль приведет к исключительной ситуации.
Попытка деления на нуль.
Начинающие программисты порой испытывают затруднения при использова нии потока Console.Error. Перед ними невольно встает вопрос: если оба потока, Console.Out и Console.Error, по умолчанию выводят результат на консоль, то за чем нужны два разных потока вывода? Ответ на этот вопрос заключается в том, что стандартные потоки могут быть переадресованы на другие устройства. Так, поток Console.Error можно переадресовать в выходной файл на диске, а не на экран. Это, например, означает, что сообщения об ошибках могут быть направлены в файл журна ла регистрации, не мешая выводу на консоль. И наоборот, если вывод на консоль пере адресуется, а вывод сообщений об ошибках остается прежним, то на консоли появятся сообщения об ошибках, а не выводимые на нее данные. Мы еще вернемся к вопросу переадресации после рассмотрения файлового ввода-вывода.
В среде .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 (указан неверный режим), SeaurityException (у пользователя нет прав доступа к файлу), 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 для открытия файла и чтения из него
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 оказывается непустым. Подобный механизм препятствует любой попытке вызвать метод Close() для переменной 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 с = 'А'; с <= 'Z'; C++)
fout.WriteByte((byte) с);
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
if (fout != null) fout.Close();
}
}
}
В данной программе сначала создается выходной файл под названием test.txt с помощью перечисляемого значения FileMode.CreateNew. Это означает, что файл с таким же именем не должен уже существовать. (В противном случае генерируется исключение IOException.) После открытия выходного файла в него записываются прописные буквы английского алфавита. По завершении данной программы содер жимое файла test.txt оказывается следующим.
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Преимущество байтового ввода-вывода средствами класса 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();
}
}
}
Несмотря на то что файлы часто обрабатываются побайтово, для этой цели можно воспользоваться также символьными потоками. Преимущество символьных потоков заключается в том, что они оперируют символами непосредственно в уникоде. Так, если требуется сохранить текст в уникоде, то для этого лучше всего подойдут именно символьные потоки. В целом, для выполнения операций символьного ввода-вывода в файлы объект класса FileStream заключается в оболочку класса StreamReader или StreamWriter. В этих классах выполняется автоматическое преобразование байтового потока в символьный и наоборот.
Не следует, однако, забывать, что на уровне операционной системы файл представ ляет собой набор байтов. И применение класса StreamReader или StreamWriter никак не может этого изменить.
Класс StreamWriter является производным от класса TextWriter, а класс StreamReader — производным от класса TextReader. Следовательно, в классах StreamReader и StreamWriter доступны методы и свойства, определенные в их ба зовых классах.
Для создания символьного потока вывода достаточно заключить объект клас са Stream, например FileStream, в оболочку класса StreamWriter. В классе StreamWriter определено несколько конструкторов. Ниже приведен едва ли не са мый распространенный среди них:
StreamWriter(Stream поток)
где поток обозначает имя открытого потока. Этот конструктор генерирует исклю чение ArgumentException, если поток не открыт для вывода, а также исключение ArgumentNullException, если поток оказывается пустым. После создания объекта класс StreamWriter выполняет автоматическое преобразование символов в байты.
Ниже приведен простой пример сервисной программы ввода с клавиатуры и выво да на диск набранных текстовых строк, сохраняемых в файле test.txt. Набираемый тест вводится до тех пор, пока в нем не встретится строка "стоп". Для символьного вы вода в файл в этой программе используется объект класса FileStream, заключенный в оболочку класса StreamWriter.
// Простая сервисная программа ввода с клавиатуры и вывода на диск,
// демонстрирующая применение класса StreamWriter.
using System;
using System.IO;
class KtoD {
static void Main() {
string str;
FileStream fout;
// Открыть сначала поток файлового ввода-вывода.
try {
fout = new FileStream("test.txt", FileMode.Create);
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\n" + exc.Message);
return;
}
// Заключить поток файлового ввода-вывода в оболочку класса StreamWriter.
StreamWriter fstr_out = new StreamWriter(fout);
try {
Console.WriteLine("Введите текст, а по окончании — 'стоп'.");
do {
Console.Write(": ");
str = Console.ReadLine();
if(str != "стоп") {
str = str + "\r\n"; // добавить новую строку
fstr_out.Write(str);
}
} while(str != "стоп");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
fstr_out.Close ();
}
}
}
В некоторых случаях файл удобнее открывать средствами самого класса StreamWriter. Для этого служит один из следующих конструкторов:
StreamWriter(string путь)
StreamWriter(string путь, bool append)
где путь — это имя открываемого файла, включая полный путь к нему. Если во вто рой форме этого конструктора значение параметра append равно true, то выводи мые данные присоединяются в конец существующего файла. В противном случае эти данные перезаписывают содержимое указанного файла. Но независимо от формы конструктора файл создается, если он не существует. При появлении ошибок ввода- вывода в обоих случаях генерируется исключение IOException. Кроме того, могут быть сгенерированы и другие исключения.
Ниже приведен вариант представленной ранее сервисной программы ввода с кла виатуры и вывода на диск, измененный таким образом, чтобы открывать выходной файл средствами самого класса StreamWriter.
// Открыть файл средствами класса StreamWriter.
using System;
using System.IO;
class KtoD {
static void Main() {
string str;
StreamWriter fstr_out = null;
try {
// Открыть файл, заключенный в оболочку класса StreamWriter.
fstr_out = new StreamWriter("test.txt");
Console.WriteLine("Введите текст, а по окончании — 'стоп'.");
do {
Console.Write(" : ");
str = Console.ReadLine();
if(str != "стоп") {
str = str + "\r\n"; // добавить новую строку
fstr_out.Write(str);
}
} while(str != "стоп");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
if(fstr_out != null) fstr_out.Close();
}
}
}
Для создания символьного потока ввода достаточно заключить байтовый поток в оболочку класса StreamReader. В классе StreamReader определено несколько кон структоров. Ниже приведен наиболее часто используемый конструктор:
StreamReader(Stream поток)
где поток обозначает имя открытого потока. Этот конструктор генерирует исключе ние ArgumentNullException, если поток оказывается пустым, а также исключение ArgumentException, если поток не открыт для ввода. После своего создания объект класса StreamReader выполняет автоматическое преобразование байтов в символы. По завершении ввода из потока типа StreamReader его нужно закрыть. При этом за крывается и базовый поток.
В приведенном ниже примере создается простая сервисная программа ввода с дис ка и вывода на экран содержимого текстового файла test.txt. Она служит дополне нием к представленной ранее сервисной программе ввода с клавиатуры и вывода на диск.
// Простая сервисная программа ввода с диска и вывода на экран,
// демонстрирующая применение класса StreamReader.
using System;
using System.IO;
class DtoS {
static void Main() {
FileStream fin;
string s;
try {
fin = new FileStream("test.txt", FileMode.Open);
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\n" + exc.Message);
return;
}
StreamReader fstr_in = new StreamReader(fin);
try {
while((s = fstr_in.ReadLine()) != null) {
Console.WriteLine(s);
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
fstr_in.Close();
}
}
}
Обратите внимание на то, как в этой программе определяется конец файла. Ког да метод ReadLine() возвращает пустую ссылку, это означает, что достигнут конец файла. Такой способ вполне работоспособен, но в классе StreamReader предостав ляется еще одно средство для обнаружения конца потока — EndOfStream. Это до ступное для чтения свойство имеет логическое значение true, когда достигается конец потока, в противном случае — логическое значение false. Следовательно, свойство EndOfStream можно использовать для отслеживания конца файла. В качестве приме ра ниже представлен другой способ организации цикла while для чтения из файла.
while(!fstr_in.EndOfStream) {
s = fstr_in.ReadLine();
Console.WriteLine(s);
}
В данном случае код немного упрощается благодаря свойству EndOfStream, хотя общий порядок выполнения операции ввода из файла не меняется. Иногда такое при менение свойства EndOfStream позволяет несколько упростить сложную ситуацию, внося ясность и улучшая структуру кода.
Иногда файл проще открыть, используя непосредственно класс StreamReader, аналогично классу StreamWriter. Для этой цели служит следующий конструктор:
StreamReader(string путь)
где путь — это имя открываемого файла, включая полный путь к нему. Указы ваемый файл должен существовать. В противном случае генерируется исключение FileNotFoundException. Если путь оказывается пустым, то генерируется исключе ние ArgumentNullException. А если путь содержит пустую строку, то генерируется исключение ArgumentException. Кроме того, могут быть сгенерированы исключения IOException и DirectoryNotFoundException.
Как упоминалось ранее, стандартные потоки, например Console.In, могут быть переадресованы. И чаще всего они переадресовываются в файл. Когда стандартный поток переадресовывается, то вводимые или выводимые данные направляются в новый поток в обход устройств, используемых по умолчанию. Благодаря переадресации стан дартных потоков в программе может быть организован ввод команд из дискового фай ла, создание файлов журнала регистрации и даже чтение входных данных из сетевого соединения.
Переадресация стандартных потоков достигается двумя способами. Прежде всего, это делается при выполнении программы из командной строки с помощью операто ров < и >, переадресовывающих потоки Console.In и Console.Out соответственно. Допустим, что имеется следующая программа.
using System;
class Test {
static void Main() {
Console.WriteLine("Это тест.");
}
}
Если выполнить эту программу из командной строки
Test > log
то символьная строка "Это тест." будет выведена в файл log. Аналогичным образом переадресуется ввод. Но для переадресации ввода указываемый источник входных дан ных должен удовлетворять требованиям программы, иначе она "зависнет".
Операторы < и >, выполняющие переадресацию из командной строки, не являются составной частью С#, а предоставляются операционной системой. Поэтому если в ра бочей среде поддерживается переадресация ввода-вывода, как, например, в Windows, то стандартные потоки ввода и вывода можно переадресовать, не внося никаких изме нений в программу. Тем не менее существует другой способ, позволяющий осущест влять переадресацию стандартных потоков под управлением самой программы. Для этого служат приведенные ниже методы SetIn(), SetOut() и SetError(), являю щиеся членами класса Console.
static void SetIn(TextReader новый_поток_ввода)
static void SetOut(TextWriter новый_поток_вывода)
static void SetError(TextWriter новый_поток_сообщений_об_ошибках)
Таким образом, для переадресации ввода вызывается метод SetIn() с указанием требуемого потока. С этой целью может быть использован любой поток ввода, при условии, что он является производным от класса TextReader. А для переадресации вывода вызывается метод SetOut() с указанием требуемого потока вывода, который должен быть также производным от класса TextReader. Так, для переадресации вы вода в файл достаточно указать объект класса FileStream, заключенный в оболочку класса StreamWriter. Соответствующий пример программы приведен ниже.
// Переадресовать поток Console.Out.
using System;
using System.IO;
class Redirect {
static void Main() {
StreamWriter log_out = null;
try {
log_out = new StreamWriter("logfile.txt");
// Переадресовать стандартный вывод в файл logfile.txt.
Console.SetOut(log_out);
Console.WriteLine("Это начало файла журнала регистрации.");
for(int i=0; i<10; i++) Console.WriteLine(i);
Console.WriteLine("Это конец файла журнала регистрации.");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\n" + exc.Message);
} finally {
if(log_out != null) log_out.Close();
}
}
}
При выполнении этой программы на экран ничего не выводится, но файл logfile.txt будет содержать следующее.
Это начало файла журнала регистрации.
0
1
2
3
4
5
6
7
8
9
Это конец файла журнала регистрации.
Попробуйте сами поупражняться в переадресации других встроенных потоков.
В приведенных ранее примерах демонстрировались возможности чтения и записи байтов или символов. Но ведь имеется также возможность (и ею пользуются часто) чи тать и записывать другие типы данных. Например, можно создать файл, содержащий данные типа int, double или short. Для чтения и записи двоичных значений встро енных в C# типов данных служат классы потоков BinaryReader и BinaryWriter. Используя эти потоки, следует иметь в виду, что данные считываются и записываются во внутреннем двоичном формате, а не в удобочитаемой текстовой форме.
Класс BinaryWriter служит оболочкой, в которую заключается байтовый поток, управляющий выводом двоичных данных. Ниже приведен наиболее часто употребляе мый конструктор этого класса:
BinaryWriter(Stream output)
где output обозначает поток, в который выводятся записываемые данные. Для записи в выходной файл в качестве параметра output может быть указан объект, создавае мый средствами класса FileStream. Если же параметр output оказывается пустым, то генерируется исключение ArgumentNullException. А если поток, определяемый параметром output, не был открыт для записи данных, то генерируется исключение ArgumentException. По завершении вывода в поток типа BinaryWriter его нужно закрыть. При этом закрывается и базовый поток.
В классе BinaryWriter определены методы, предназначенные для записи данных всех встроенных в C# типов. Некоторые из этих методов перечислены в табл. 14.5. Обратите внимание на то, что строковые данные типа string записываются во внутреннем формате с указанием длины строки. Кроме того, в классе BinaryWriter определены стандартные методы Close() и Flush(), действующие аналогично описанному выше.
Таблица 14.5. Наиболее часто используемые методы, определенные в классе BinaryWriter
Метод | Описание |
---|---|
void Write(sbyte value) | Записывает значение типа sbyte со знаком |
void Write(byte value) | Записывает значение типа byte без знака |
void Write(byte[] buffer) | Записывает массив значений типа byte |
void Write(short value) | Записывает целочисленное значение типа short (короткое целое) |
void Write(ushort value) | Записывает целочисленное значение типа ushort (короткое целое без знака) |
void Write(int value) | Записывает целочисленное значение типа int |
void Write(uint value) | Записывает целочисленное значение типа uint (целое без знака) |
void Write(long value) | Записывает целочисленное значение типа long (длинное целое) |
void Write(ulong value) | Записывает целочисленное значение типа ulong (длинное целое без знака) |
void Write(float value) | Записывает значение типа float (с плавающей точкой одинарной точности) |
void Write(double value) | Записывает значение типа double (с плавающей точкой двойной точности) |
void Write(decimal value) | Записывает значение типа decimal (с двумя десятичными разрядами после запятой) |
void Write(char ch) | Записывает символ |
void Write(char[] buffer) | Записывает массив символов |
void Write(string value) | Записывает строковое значение типа string, представленное во внутреннем формате с указанием длины строки |
Класс BinaryReader служит оболочкой, в которую заключается байтовый поток, управляющий вводом двоичных данных. Ниже приведен наиболее часто употребляе мый конструктор этого класса:
BinaryReader(Stream input)
где input обозначает поток, из которого вводятся считываемые данные. Для чтения из входного файла в качестве параметра input может быть указан объект, созда ваемый средствами класса FileStream. Если же поток, определяемый параметром input, не был открыт для чтения данных или оказался недоступным по иным при чинам, то генерируется исключение ArgumentException. По завершении ввода из потока типа BinaryReader его нужно закрыть. При этом закрывается и базовый поток.
В классе BinaryReader определены методы, предназначенные для чтения дан ных всех встроенных в C# типов. Некоторые из этих методов перечислены в табл. 14.6. Следует, однако, иметь в виду, что в методе Readstring() считывается символьная строка, хранящаяся во внутреннем формате с указанием ее длины. Все методы данного класса генерируют исключение IOException, если возникает ошибка ввода. Кроме того, могут быть сгенерированы и другие исключения.
Таблица 14.6. Наиболее часто используемые методы, определенные в классе BinaryReader
Метод | Описание |
---|---|
bool ReadBoolean() | Считывает значение логического типа bool |
byte ReadByte() | Считывает значение типа byte |
sbyte ReadSByte() | Считывает значение типа sbyte |
byte[] ReadBytes(int count) | Считывает количество count байтов и возвращает их в виде массива |
char ReadChar() | Считывает значение типа char |
char[] ReadChars(int count) | Считывает количество count символов и возвращает их в виде массива |
decimal ReadDecimal() | Считывает значение типа decimal |
double ReadDouble() | Считывает значение типа double |
float ReadSingle() | Считывает значение типа float |
short ReadInt16() | Считывает значение типа short |
int ReadInt32() | Считывает значение типа int |
long ReadInt64() | Считывает значение типа long |
ushort ReadUIntl6() | Считывает значение типа ushort |
uint ReadUInt32() | Считывает значение типа uint |
ulong ReadUInt64() | Считывает значение типа ulong |
string ReadString() | Считывает значение типа string, представленное во внутреннем двоичном формате с указанием длины строки. Этот метод следует использовать для считывания строки, которая была записана средствами класса BinaryWriter |
В классе BinaryWriter определены также три приведенных ниже варианта метода Read().
При неудачном исходе операции чтения эти методы генерируют исключение IOException. Кроме того, в классе BinaryReader определен стандартный метод Close().
Метод | Описание |
---|---|
int Read() | Возвращает целочисленное представление следующего доступного символа из вызывающего потока ввода. При обнаружении конца файла возвращает значение -1 |
int Read(byte[] buffer, int offset, int count) | Делает попытку прочитать количество count байтов в массив buffer, начиная с элемента buffer[offset], и возвращает количество успешно считанных байтов |
int Read(char[]buffer, int offset, int count) | Делает попытку прочитать количество count символов в массив buffer, начиная с элемента buffer[offset], и возвращает количество успешно считанных символов |
Ниже приведен пример программы, в котором демонстрируется применение клас сов BinaryReader и BinaryWriter для двоичного ввода-вывода. В этой программе в файл записываются и считываются обратно данные самых разных типов. // Записать двоичные данные, а затем считать их обратно.
using System;
using System.IO;
class RWData {
static void Main() {
BinaryWriter dataOut;
BinaryReader dataIn;
int i = 10;
double d = 1023.56;
bool b = true;
string str = "Это тест";
// Открыть файл для вывода.
try {
dataOut = new
BinaryWriter(new FileStream("testdata", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\n" + exc.Message);
return;
}
// Записать данные в файл.
try {
Console.WriteLine("Запись " + i);
dataOut.Write(i);
Console.WriteLine("Запись " + d);
dataOut.Write(d);
Console.WriteLine("Запись " + b);
dataOut.Write(b);
Console.WriteLine("Запись " + 12.2 * 7.4);
dataOut.Write(12.2 * 7.4);
Console.WriteLine("Запись " + str);
dataOut.Write(str);
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine();
// А теперь прочитать данные из файла.
try {
dataIn = new
BinaryReader(new FileStream("testdata", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\n" + exc.Message);
return;
}
try {
i = dataIn.ReadInt32();
Console.WriteLine("Чтение " + i);
d = dataIn.ReadDouble();
Console.WriteLine("Чтение " + d);
b = dataIn.ReadBoolean () ;
Console.WriteLine("Чтение " + b);
d = dataIn.ReadDouble();
Console.WriteLine("Чтение " + d);
str = dataIn.ReadString();
Console.WriteLine("Чтение " + str);
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
dataIn.Close();
}
}
}
Вот к какому результату приводит выполнение этой программы.
Запись 10
Запись 1023.56
Запись True
Запись 90.28
Запись Это тест
Чтение 10
Чтение 1023.56
Чтение True
Чтение 90.28
Чтение Это тест
Если просмотреть содержимое файла testdata, который получается при выпол нении этой программы, то можно обнаружить, что он содержит данные в двоичной, а не в удобочитаемой текстовой форме.
Далее следует более практический пример, демонстрирующий, насколько эффек тивным может быть двоичный ввод-вывод. Для учета каждого предмета хранения на складе в приведенной ниже программе сначала запоминается наименование предме та, имеющееся в наличии, количество и стоимость, а затем пользователю предлагается ввести наименование предмета, чтобы найти его в базе данных. Если предмет найден, отображаются сведения о его запасах на складе.
/* Использовать классы BinaryReader и BinaryWriter для
реализации простой программы учета товарных запасов. */
using System;
using System.IO;
class Inventory {
static void Main() {
BinaryWriter dataOut;
BinaryReader detaIn;
string item; // наименование предмета
int onhand; // имеющееся в наличии количество
double cost; // цена
try {
dataOut = new
BinaryWriter(new FileStream("inventory.dat", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("He удается открыть файл " +
"товарных запасов для вывода");
Console.WriteLine("Причина: " + exc.Message);
return;
}
// Записать данные о товарных запасах в файл.
try {
dataOut.Write("Молотки");
dataOut.Write(10);
dataOut.Write(3.95);
dataOut.Write("Отвертки");
dataOut.Write(18);
dataOut.Write(1.50);
dataOut.Write("Плоскогубцы");
dataOut.Write(5);
dataOut.Write(4.95);
dataOut.Write("Пилы");
dataOut.Write(8);
dataOut.Write(8.95);
}
catch(IOException exc) {
Console.WriteLine("Ошибка записи в файл товарных запасов");
Console.WriteLine("Причина: " + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine();
// А теперь открыть файл товарных запасов для чтения.
try {
dataIn = new
BinaryReader(new FileStream("inventory.dat", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("He удается открыть файл " +
"товарных запасов для ввода");
Console.WriteLine("Причина: " + exc.Message);
return;
}
// Найти предмет, введенный пользователем.
Console.Write("Введите наименование для поиска: ");
string what = Console.ReadLine();
Console.WriteLine();
try {
for(;;) {
// Читать данные о предмете хранения.
item = dataIn.ReadString();
onhand = dataIn.ReadInt32();
cost = dataIn.ReadDouble();
// Проверить, совпадает ли он с запрашиваемым предметом.
// Если совпадает, то отобразить сведения о нем.
if(item.Equals(what, StringComparison.OrdinalIgnoreCase)) {
Console.WriteLine(item + ": " + onhand + " штук в наличии. " +
"Цена: {0:С} за штуку", cost);
Console.WriteLine("Общая стоимость по наименованию <{0}>: {1:С}.",
item, cost * onhand);
break;
}
}
}
catch(EndOfStreamException) {
Console.WriteLine("Предмет не найден.");
}
catch(IOException exc) {
Console.WriteLine("Ошибка чтения из файла товарных запасов");
Console.WriteLine("Причина: " + exc.Message);
} finally {
dataIn.Glose();
}
}
}
Выполнение этой программы может привести, например, к следующему результату.
Введите наименование для поиска: Отвертки
Отвертки: 18 штук в наличии. Цена: $1.50 за штуку.
Общая стоимость по наименованию <Отвертки>: $27.00.
Обратите внимание на то, что сведения о товарных запасах сохраняются в этой про грамме в двоичном формате, а не в удобной для чтения текстовой форме. Благодаря этому обработка числовых данных может выполняться без предварительного их пре образования из текстовой формы.
Обратите также внимание на то, как в этой программе обнаруживается конец фай ла. Методы двоичного ввода генерируют исключение EndOfStreamException по до стижении конца потока, и поэтому файл читается до тех пор, пока не будет найден искомый предмет или сгенерировано данное исключение. Таким образом, для обна ружения конца файла никакого специального механизма не требуется.
В предыдущих примерах использовались последовательные файлы, т.е. файлы со строго линейным доступом, байт за байтом. Но доступ к содержимому файла может быть и произвольным. Для этого служит, в частности, метод Seek(), определенный в классе FileStream. Этот метод позволяет установить указатель положения в файле, или так называемый указатель файла, на любое место в файле. Ниже приведена общая форма метода Seek():
long Seek(long offset, SeekOrigin origin)
где offset обозначает новое положение указателя файла в байтах относительно за данного начала отсчета (origin). В качестве origin может быть указано одно из при веденных ниже значений, определяемых в перечислении SeekOrigin.
Значение | Описание |
---|---|
SeekOrigin.Begin | Поиск от начала файла |
SeekOrigin.Current | Поиск от текущего положения |
SeekOrigin.End | Поиск от конца файла |
Следующая операция чтения или записи после вызова метода Seek() будет выпол няться, начиная с нового положения в файле, возвращаемого этим методом. Если во время поиска в файле возникает ошибка, то генерируется исключение IOException. Если же запрос положения в файле не поддерживается базовым потоком, то генери руется исключение NotSupportedException. Кроме того, могут быть сгенерированы и другие исключения.
В приведенном ниже примере программы демонстрируется ввод-вывод в файл с произвольным доступом. Сначала в файл записываются прописные буквы английско го алфавита, а затем его содержимое считывается обратно в произвольном порядке.
// Продемонстрировать произвольный доступ к файлу.
using System;
using System.IO;
class RandomAccessDemo {
static void Main() {
FileStream f = null;
char ch;
try {
f = new FileStream("random.dat", FileMode.Create);
// Записать английский алфавит в файл.
for (int i=0; i < 26; i++)
f.WriteByte((byte)('A'+i));
// А теперь считать отдельные буквы английского алфавита.
f.Seek(0, SeekOrigin.Begin); // найти первый байт
ch = (char) f.ReadByte();
Console.WriteLine("Первая буква: " + ch);
f.Seek(1, SeekOrigin.Begin); // найти второй байт
ch = (char) f.ReadByte();
Console.WriteLine("Вторая буква: " + ch);
f.Seek(4, SeekOrigin.Begin); // найти пятый байт
ch = (char) f.ReadByte();
Console.WriteLine("Пятая буква: " + ch);
Console.WriteLine ();
// А теперь прочитать буквы английского алфавита через одну.
Console.WriteLine("Буквы алфавита через одну: ");
for(int i=0; i < 26; i += 2) {
f.Seek(i, SeekOrigin.Begin); // найти i-й символ
ch = (char) f.ReadByte();
Console.Write(ch + " ");
}
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\n" + exc.Message);
} finally {
if(f != null) f.Close();
}
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
Первая буква: А
Вторая буква: В
Пятая буква: Е
Буквы алфавита через одну:
А C E G I K M O Q S U W Y
Несмотря на то что метод Seek() имеет немало преимуществ при использовании с файлами, существует и другой способ установки текущего положения в файле с по мощью свойства Position. Как следует из табл. 14.2, свойство Position доступно как для чтения, так и для записи. Поэтому с его помощью можно получить или же установить текущее положение в файле. В качестве примера ниже приведен фрагмент кода из предыдущей программы записи и чтения из файла с произвольным досту пом random.dat, измененный с целью продемонстрировать применение свойства Position.
Console.WriteLine("Буквы алфавита через одну: ");
for(int i=0; i < 26; i += 2) {
f.Position = i; // найти i-й символ посредством свойства Position
ch = (char) f.ReadByte();
Console.Write(ch + " ");
}
Иногда оказывается полезно читать вводимые данные из массива или записывать выводимые данные в массив, а не вводить их непосредственно из устройства или вы водить прямо на него. Для этой цели служит класс MemoryStream. Он представляет собой реализацию класса Stream, в которой массив байтов используется для ввода и вывода. В классе MemoryStream определено несколько конструкторов. Ниже пред ставлен один из них:
MemoryStream(byte[] buffer)
где buffer обозначает массив байтов, используемый в качестве источника или адре сата в запросах ввода-вывода. Используя этот конструктор, следует иметь в виду, что массив buffer должен быть достаточно большим для хранения направляемых в него данных.
В качестве примера ниже приведена программа, демонстрирующая применение класса MemoryStream в операциях ввода-вывода.
// Продемонстрировать применение класса MemoryStream.
using System;
using System.IO;
class MemStrDemo {
static void Main() {
byte[] storage = new byte[255];
// Создать запоминающий поток.
MemoryStream memstrm = new MemoryStream(storage);
// Заключить объект memstrm в оболочки классов
// чтения и записи данных в потоки.
StreamWriter memwtr = new StreamWriter(memstrm);
StreamReader memrdr = new StreamReader(memstrm);
try {
// Записать данные в память, используя объект memwtr.
for(int i=0; i < 10; i++)
memwtr.WriteLine("byte [" + i + "]: " + i);
// Поставить в конце точку.
memwtr.WriteLine(".");
memwtr.Flush();
Console.WriteLine("Чтение прямо из массива storage: ");
// Отобразить содержимое массива storage непосредственно,
foreach(char ch in storage) {
if (ch == '.') break;
Console.Write(ch);
}
Console.WriteLine("\nЧтение из потока с помощью объекта memrdr: ");
// Читать из объекта memstrm средствами ввода данных из потока.
memstrm.Seek(0, SeekOrigin.Begin); // установить указатель файла
// в исходное положение
string str = memrdr.ReadLine();
while(str != null) {
str = memrdr .ReadLine();
if (str[0] == '.') break;
Console.WriteLine(str);
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\n" + exc.Message);
} finally {
// Освободить ресурсы считывающего и записывающего потоков.
memwtr.Close();
memrdr.Close();
}
}
}
Вот к какому результату приводит выполнение этой программы.
Чтение прямо из массива storage:
byte [0]: 0
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4]: 4
byte [5]: 5
byte [6]: 6
byte [7]: 7
byte [8]: 8
byte [9]: 9
Чтение из потока с помощью объекта memrdr:
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4]: 4
byte [5]: 5
byte [6]: 6
byte [7]: 7
byte [8]: 8
byte [9]: 9
В этой программе сначала создается массив байтов, называемый storage. Затем этот массив используется в качестве основной памяти для объекта memstrm класса MemoryStream. Из объекта memstrm, в свою очередь, создаются объекты memrdr клас са StreamReader и memwtr класса StreamWriter. С помощью объекта memwtr выво димые данные записываются в запоминающий поток. Обратите внимание на то, что после записи выводимых данных для объекта memwtr вызывается метод Flush(). Это необходимо для того, чтобы содержимое буфера этого объекта записывалось непо средственно в базовый массив. Далее содержимое базового массива байтов отобража ется вручную в цикле foreach. После этого указатель файла устанавливается с по мощью метода Seek() в начало запоминающего потока, из которого затем вводятся данные с помощью объекта потока memrdr.
Запоминающие потоки очень полезны для программирования. С их помощью можно, например, организовать сложный вывод с предварительным накоплением данных в массиве до тех пор, пока они не понадобятся. Этот прием особенно поле зен для программирования в такой среде с графическим пользовательским интер фейсом, как Windows. Кроме того, стандартный поток может быть переадресован из массива. Это может пригодиться, например, для подачи тестовой информации в программу.
Для выполнения операций ввода-вывода с запоминанием в некоторых приложе ниях в качестве базовой памяти иногда лучше использовать массив типа string, чем массив типа byte. Именно для таких случаев и предусмотрены классы StringReader и StringWriter. В частности, класс StringReader наследует от класса TextReader, а класс StringWriter — от класса TextWriter. Следовательно, они представля ют собой потоки, имеющие доступ к методам, определенным в этих двух базовых классах, что позволяет, например, вызывать метод ReadLine() для объекта класса StringReader, а метод WriteLine() — для объекта класса StringWriter.
Ниже приведен конструктор класса StringReader:
StringReader(string s)
где s обозначает символьную строку, из которой производится чтение.
В классе StringWriter определено несколько конструкторов. Ниже представлен один из наиболее часто используемых.
StringWriter()
Этот конструктор создает записывающий поток, который помещает выводимые данные в строку. Для получения содержимого этой строки достаточно вызвать метод ToString().
Ниже приведен пример, демонстрирующий применение классов StringReader и StringWriter.
// // Продемонстрировать применение классов StringReader и StringWriter.
using System;
using System.IO;
class StrRdrWtrDemo {
static void Main() {
StringWriter strwtr = null;
StringReader strrdr = null;
try {
// Создать объект класса StringWriter.
strwtr = new StringWriter();
// Вывести данные в записывающий поток типа StringWriter.
for (int i=0; i < 10; i++)
strwtr.WriteLine("Значение i равно: " + i);
// Создать объект класса StringReader.
strrdr = new StringReader(strwtr.ToString());
//А теперь ввести данные из считывающего потока типа StringReader.
string str = strrdr.ReadLine();
while(str != null) {
str = strrdr.ReadLine();
Console.WriteLine(str);
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\n" + exc.Message);
} finally {
// Освободить ресурсы считывающего и записывающего потоков.
if(strrdr != null) strrdr.Close();
if(strwtr != null) strwtr.Close();
}
}
}
Вот к каком результату приводит выполнение этого кода.
Значение i равно: 1
Значение i равно: 2
Значение i равно: 3
Значение i равно: 4
Значение i равно: 5
Значение i равно: 6
Значение i равно: 7
Значение i равно: 8
Значение i равно: 9
В данном примере сначала создается объект strwtr класса StringWriter, в кото рый выводятся данные с помощью метода WriteLine(). Затем создается объект класса StringReader с использованием символьной строки, содержащейся в объекте strwtr. Эта строка получается в результате вызова метода ToString() для объекта strwtr. И наконец, содержимое данной строки считывается с помощью метода ReadLine().
В среде .NET Framework определен класс File, который может оказаться полезным для работы с файлами, поскольку он содержит несколько статических методов, выпол няющих типичные операции над файлами. В частности, в классе File имеются методы для копирования и перемещения, шифрования и расшифровывания, удаления фай лов, а также для получения и задания информации о файлах, включая сведения об их существовании, времени создания, последнего доступа и различные атрибуты файлов (только для чтения, скрытых и пр.). Кроме того, в классе File имеется ряд удобных ме тодов для чтения из файлов и записи в них, открытия файла и получения ссылки типа FileStream на него. В классе File содержится слишком много методов для подроб ного их рассмотрения, поэтому мы уделим внимание только трем из них. Сначала бу дет представлен метод Сору(), а затем — методы Exists() и GetLastAccessTime(). На примере этих методов вы сможете получить ясное представление о том, насколь ко удобны методы, доступные в классе File. И тогда вам станет ясно, что класс File определенно заслуживает более тщательного изучения.
СОВЕТ Ряд методов для работы с файлами определен также в классе FileInfo. Этот класс отли чается от класса File одним, очень важным преимуществом: для операций над файлами он пре доставляет методы экземпляра и свойства, а не статические методы. Поэтому для выполнения нескольких операций над одним и тем же файлом лучше воспользоваться классом FileInfo.
Ранее в этой главе демонстрировался пример программы, в которой файл копиро вался вручную путем чтения байтов из одного файла и записи в другой. И хотя задача копирования файлов не представляет особых трудностей, ее можно полностью авто матизировать с помощью метода Сору(), определенного в классе File. Ниже пред ставлены две формы его объявления.
static void Copy (string имя_исходного_файла, string имя_целевого_файла)
static void Copy (string имя_исходного_файла, string имя_целевого_файла, boolean overwrite)
Метод Copy() копирует файл, на который указывает имя_исходного_файла, в файл, на который указывает имя_целевого_файла. В первой форме данный метод копирует файл только в том случае, если файл, на который указывает имя_целево- го_файла, еще не существует. А во второй форме копия заменяет и перезаписывает целевой файл, если он существует и если параметр overwrite принимает логическое значение true. Но в обоих случаях может быть сгенерировано несколько видов исклю чений, включая IOException и FileNotFoundException.
В приведенном ниже примере программы метод Сору() применяется для копи рования файла. Имена исходного и целевого файлов указываются в командной строке. Обратите внимание, насколько эта программа короче демонстрировавшейся ранее. Кроме того, она более эффективна.
/* Скопировать файл, используя метод File.Copy().
Чтобы воспользоваться этой программой, укажите имя исходного и целевого
файлов. Например, чтобы скопировать файл FIRST.DAT в файл SECOND.DAT,
введите в командной строке следующее:
CopyFile FIRST.DAT SECOND.DAT
*/
using System;
using System.IO;
class CopyFile {
static void Main(string[] args) {
if(args.Length != 2) {
Console.WriteLine("Применение : CopyFile Откуда Куда");
return;
}
// Копировать файлы.
try {
File.Copy(args[0], args[1]);
} catch(IOException exc) {
Console.WriteLine("Ошибка копирования файла\n" + exc.Message);
}
}
}
Как видите, в этой программе не нужно создавать поток типа FileStream или освобождать его ресурсы. Все это делается в методе Сору() автоматически. Обратите также внимание на то, что в данной программе существующий файл не перезаписыва ется. Поэтому если целевой файл должен быть перезаписан, то для этой цели лучше воспользоваться второй из упоминавшихся ранее форм метода Сору().
С помощью методов класса File очень легко получить нужные сведения о файле. Рассмотрим два таких метода: Exists() и GetLastAccessTime(). Метод Exists() определяет, существует ли файл, а метод GetLastAccessTime() возвращает дату и вре мя последнего доступа к файлу. Ниже приведены формы объявления обоих методов.
static bool Exists(string путь)
static DateTime GetLastAccessTime(string путь)
В обоих методах путь обозначает файл, сведения о котором требуется получить. Метод Exists() возвращает логическое значение true, если файл существует и до ступен для вызывающего процесса. А метод GetLastAccessTime() возвращает струк туру DateTime, содержащую дату и время последнего доступа к файлу. (Структура DateTime описывается далее в этой книге, но метод ToString() автоматически при водит дату и время к удобочитаемому виду.) С указанием недействительных аргумен тов или прав доступа при вызове обоих рассматриваемых здесь методов может быть связан целый ряд исключений, но в действительности генерируется только исключе ние IOException.
В приведенном ниже примере программы методы Exists() и GetLastAccessTime() демонстрируются в действии. В этой программе сначала определяется, существует ли файл под названием test.txt. Если он существует, то на экран выводит время послед него доступа к нему.
// Применить методы Exists() и GetLastAccessTime().
using System;
using System.IO;
class ExistsDemo {
static void Main() {
if(File.Exists("test.txt"))
Console.WriteLine("Файл существует. В последний раз он был доступен " +
File.GetLastAccessTime("test.txt"));
else
Console.WriteLine("Файл не существует");
}
}
Кроме того, время создания файла можно выяснить, вызвав метод GetCreationTime(), а время последней записи в файл, вызвав метод GetLastWriteTime(). Имеются так же варианты этих методов для представления данных о файле в формате всеобще го скоординированного времени (UTC). Попробуйте поэкспериментировать с ними.
Прежде чем завершить обсуждение темы ввода-вывода, рассмотрим еще один спо соб, который может пригодиться при чтении числовых строк. Как вам должно быть уже известно, метод WriteLine() предоставляет удобные средства для вывода раз личных типов данных на консоль, включая и числовые значения встроенных типов, на пример int или double. При этом числовые значения автоматически преобразуются методом WriteLine() в удобную для чтения текстовую форму. В то же время ана логичный метод ввода для чтения и преобразования строк с числовыми значениями в двоичный формат их внутреннего представления не предоставляется. В частности, отсутствует вариант метода Read() специально для чтения строки "100", введенной с клавиатуры, и автоматического ее преобразования в соответствующее двоичное зна чение, которое может быть затем сохранено в переменной типа int. Поэтому данную задачу приходится решать другими способами. И самый простой из них — воспользо ваться методом Parse(), определенным для всех встроенных числовых типов данных.
Прежде всего необходимо отметить следующий важный факт: все встроенные в C# типы данных, например int или double, на самом деле являются не более чем псев донимами (т.е. другими именами) структур, определяемых в среде .NET Framework. В действительности тип в C# невозможно отличить от типа структуры в среде .NET Framework, поскольку один просто носит имя другого. В C# для поддержки значений простых типов используются структуры, и поэтому для типов этих значений имеются специально определенные члены структур.
Ниже приведены имена структур .NET и их эквиваленты в виде ключевых слов C# для числовых типов данных.
Имя структуры в .NET | Имя типа данных в C# |
---|---|
Decimal | decimal |
Double | double |
Single | float |
Int16 | short |
Int32 | int |
Int64 | long |
UInt16 | ushort |
UInt32 | uint |
Uint64 | ulong |
Byte | byte |
Sbyte | sbyte |
Эти структуры определены в пространстве имен System. Следовательно, имя струк туры Int32 полностью определяется как System.Int32. Эти структуры предоставля ют обширный ряд методов, помогающих полностью интегрировать значения простых типов в иерархию объектов С#. А кроме того, в числовых структурах определяется ста тический метод Parse(), преобразующий числовую строку в соответствующий дво ичный эквивалент.
Существует несколько перегружаемых форм метода Parse(). Ниже приведены его простейшие варианты для каждой числовой структуры. Они выполняют преобразова ние с учетом местной специфики представления чисел. Следует иметь в виду, что каж дый метод возвращает двоичное значение, соответствующее преобразуемой строке.
Структура | Метод преобразования |
---|---|
Decimal | static decimal Parse(string s) |
Double | static double Parse(string s) |
Single | static float Parse(string s) |
Int64 | static long Parse(string s) |
Int32 | static int Parse(string s) |
Intl6 | static short Parse(string s) |
UInt64 | static ulong Parse(string s) |
UInt32 | static uint Parse(string s) |
Ulnt16 | static ushort Parse(string s) |
Byte | static byte Parse(string s) |
Sbyte | static sbyte Parse(string s) |
Приведенные выше варианты метода Parse() генерируют исключение FormatException, если строка s не содержит допустимое число, определяемое вы зывающим типом данных. А если она содержит пустое значение, то генерируется ис ключение ArgumentNullException. Когда же значение в строке s превышает допу стимый диапазон чисел для вызывающего типа данных, то генерируется исключение OverflowException.
Методы синтаксического анализа позволяют без особого труда преобразовать чис ловое значение, введенное с клавиатуры или же считанное из текстового файла в виде строки, в соответствующий внутренний формат. В качестве примера ниже приведена программа, в которой усредняется ряд чисел, вводимых пользователем. Сначала поль зователю предлагается указать количество усредняемых значений, а затем это количе ство считывается методом ReadLine() и преобразуется из строки в целое число ме тодом Int32.Parse(). Далее вводятся отдельные значения, преобразуемые методом Double.Parse() из строки в их эквивалент типа double.
// Эта программа усредняет ряд чисел, вводимых пользователем.
using System;
using System.IO;
class AvgNums {
static void Main() {
string str;
int n;
double sum = 0.0;
double avg, t;
Console.Write("Сколько чисел вы собираетесь ввести: ");
str = Console.ReadLine();
try {
n = Int32.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
return;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message);
return;
}
Console.WriteLine("Введите " + n + " чисел.");
for(int i=0; i < n ; i++) {
Console.Write(": ");
str = Console.ReadLine();
try {
t = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
t = 0.0;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message);
t = 0;
}
sum += t;
}
avg = sum / n;
Console.WriteLine("Среднее равно " + avg);
}
}
Выполнение этой программы может привести, например, к следующему резуль тату.
Сколько чисел вы собираетесь ввести: 5
Введите 5 чисел.
: 1.1
: 2.2
: 3.3
: 4.4
: 5.5
Среднее равно 3.3
Следует особо подчеркнуть, что для каждого преобразуемого значения необхо димо выбирать подходящий метод синтаксического анализа. Так, если попытаться преобразовать строку, содержащую значение с плавающей точкой, методом Int32. Parse(), то искомый результат, т.е. числовое значение с плавающей точкой, получить не удастся.
Как пояснялось выше, при неудачном исходе преобразования метод Parse() сге нерирует исключение. Для того чтобы избежать генерирования исключений при пре образовании числовых строк, можно воспользоваться методом TryParse(), опреде ленным для всех числовых структур. В качестве примера ниже приведен один из вари антов метода TryParse(), определяемых в структуре Int32:
static bool TryParse(string s, out int результат)
где s обозначает числовую строку, передаваемую данному методу, который возвра щает соответствующий результат после преобразования с учетом выбираемой по умолчанию местной специфики представления чисел. (Конкретную местную специ фику представления чисел с учетом региональных стандартов можно указать в другом варианте данного метода.) При неудачном исходе преобразования, например, когда параметр s не содержит числовую строку в надлежащей форме, метод TryParse() возвращает логическое значение false. В противном случае он возвращает логическое значение true. Следовательно, значение, возвращаемое этим методом, обязательно следует проверить, чтобы убедиться в удачном (или неудачном) исходе преобразова ния.