Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
383 об ошибках будут появляться на консольном устройстве, и их легко увидеть. Но подробнее о перенаправлении потоков мы поговорим ниже, когда рассмотрим файловый ввод-вывод. Класс FileStream и файловый ввод-вывод на побайтовой основе В C# предусмотрены классы, которые позволяют считывать содержимое файлов и записывать в них информацию. Конечно же, дисковые файлы — самый распространенный тип файлов. На уровне операционной системы все файлы обрабатываются на побайтовой основе. Нетрудно предположить, что в C# определены методы, предназначенные для считывания байтов из файла и записи байтов в файл. Таким образом, файловые операции чтения и записи с использованием байтовых потоков очень востребованы. C# также позволяет поместить файловый поток с ориентацией на побайтовую обработку в символьный объект. Файловые операции, ориентированные на символы, используются в случае текстовых файлов. Символьные потоки рассматриваются ниже в этой главе. А пока изучим ввод-вывод данных на побайтовой основе. Чтобы создать байтовый поток с привязкой к файлу, используйте класс FileStream . Класс FileStream — производный от Stream и потому обладает функциональными возможностями базового класса. Помните, что потоковые классы, включая FileStream , определены в пространстве имен System.IO . Следовательно, при их использовании в начало программы вы должны включить следующую инструкцию: using System.IO; Как открыть и закрыть файл Чтобы создать байтовый поток, связанный с файлом, создайте объект класса FileStream . В классе FileStream определено несколько конструкторов. Чаще всего из них используется следующий: FileStream(string filename , FileMode mode ) Здесь элемент filename означает имя файла, который необходимо открыть, причем оно может включать полный путь к файлу. Элемент mode означает, как именно должен быть открыт этот файл, или режим открытия. Элемент mode может принимать одно из значений, определенных перечислением FileMode (они описаны в табл. 14.4). Этот конструктор открывает файл для доступа с разрешением чтения и записи. Таблица 14.4. Значения перечисления FileMode Значение Описание FileMode.Append Добавляет выходные данные в конец файла FileMode.Create Создает новый выходной файл. Существующий файл с таким же именем будет разрушен FileMode.CreateNew Создает новый выходной файл. Файл с таким же именем не должен существовать FileMode.Open Открывает существующий файл FileMode.OpenOrCreate Открывает файл, если он существует, В противном случае создает новый FileMode.Truncate Открывает существующий файл, но усекает его длину до нуля 384 Часть I. Язык C# Если попытка открыть файл оказалось неуспешной, генерируется исключение. Если файл невозможно открыть по причине его отсутствия, генерируется исключение типа FileNotFoundException . Если файл невозможно открыть из-за ошибки ввода-вывода, генерируется исключение типа IOException . Возможны также исключения следующих типов: ArgumentNullException (если имя файла представляет собой null -значение), ArgumentException (если некорректен параметр mode ), SecurityException (если пользователь не обладает правами доступа) и DirectoryNotFoundException (если некорректно задан каталог). В следующем фрагменте программы показан один из способов открыть файл test.dat для ввода данных. FileStream fin; try { fin = new FileStream("test.dat", FileMode.Open); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message); return; } catch { Console.WriteLine("Не удается открыть файл."); return; } Здесь первая catch -инструкция перехватывает ошибку, связанную с отсутствием файла. Вторая, предназначенная для “всеобщего перехвата”, обрабатывает другие ошибки, которые возможны при работе с файлами. Конечно, можно было бы отслеживать возникновение каждой ошибки в отдельности, сообщая о возникшей проблеме. Но ради простоты во всех примерах этой книги организован перехват исключений только типа FileNotFoundException или IOException , но в реальных приложениях (в зависимости от обстоятельств) вам, скорее всего, придется обрабатывать другие возможные исключения. Как уже упоминалось, приведенный выше конструктор FileStream открывает файл с доступом для чтения и записи. Если необходимо ограничить доступ только чтением или только записью, используйте следующий конструктор: FileStream(string filename , FileMode mode , FileAccess how ) Как и прежде, элемент filename означает имя открываемого файла, a mode — способ его открытия. Значение, передаваемое с помощью параметра how , определяет способ доступа к файлу. Этот параметр может принимать одно из значений, определенных перечислением FileAccess , а именно: FileAccess.Read FileAccess.Write FileAccess.ReadWrite Например, при выполнении следующей инструкции файл test.dat будет открыт только для чтения: FileStream fin = new FileStream("test.dat", FileMode.Open, FileAccess.Read); По завершении работы с файлом его необходимо закрыть. Для этого достаточно вызвать метод Close() . Его общая форма вызова имеет такой вид: void Close() При закрытии файла освобождаются системные ресурсы, ранее выделенные для этого файла, что дает возможность использовать их для других файлов. Метод Close() может генерировать исключение типа IOException Глава 14. Использование средств ввода-вывода 385 Считывание байтов из объекта класса FileStream В классе FileStream определены два метода, которые считывают байты из файла: ReadByte() и Read() . Чтобы прочитать из файла один байт, используйте метод ReadByte() , общая форма вызова которого имеет следующий вид: int ReadByte() При каждом вызове этого метода из файла считывается один байт, и метод возвращает его как целочисленное значение. При обнаружении конца файла метод возвращает -1 . Метод может генерировать исключения типов NotSupportedException (поток не открыт для ввода) и ObjectDisposedException (поток закрыт). Чтобы считать блок байтов, используйте метод Read() , общая форма вызова которого такова: int Read(byte[] buf , int offset , int numBytes ) Метод Read() пытается считать numBytes байтов в массив buf , начиная с элемента buf[offset] . Он возвращает количество успешно считанных байтов. При возникновении ошибки ввода-вывода генерируется исключение типа IOException Помимо прочих, возможно также генерирование исключения типа NotSupportedException , если используемый поток не поддерживает операцию считывания данных. В следующей программе метод ReadByte() используется для ввода содержимого текстового файла и его отображения. Имя файла задается в качестве аргумента командной строки. Обратите внимание на try/catch -блоки, которые обрабатывают две ошибки, возможные при первоначальном выполнении этой программы: “указанный файл не найден” или “пользователь забыл указать имя файла”. Такой подход обычно полезен при использовании аргументов командной строки. /* Отображение содержимого текстового файла. Чтобы использовать эту программу, укажите имя файла, содержимое которого вы хотите увидеть. Например, чтобы увидеть содержимое файла TEST.CS, используйте следующую командную строку: ShowFile TEST.CS */ using System; using System.IO; class ShowFile { public static void Main(string[] args) { int i; FileStream fin; try { fin = new FileStream(args[0], FileMode.Open); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message); return; } catch(IndexOutOfRangeException exc) { Console.WriteLine(exc.Message + "\nПрименение: ShowFile Файл"); return; } 386 Часть I. Язык C# // Считываем байты до тех пор, пока не встретится EOF. do { try { i = fin.ReadByte(); } catch(Exception exc) { Console.WriteLine(exc.Message); return; } if(i != -1) Console.Write((char) i); } while(i != -1); fin.Close(); } } Запись данных в файл Чтобы записать байт в файл, используйте метод WriteByte() . Простейшая его форма имеет следующий вид: void WriteByte(byte val ) Этот метод записывает в файл байт, заданный параметром val . При возникновении во время записи ошибки генерируется исключение типа IOException . Если соответствующий поток не открыт для вывода данных, генерируется исключение типа NotSupportedException С помощью метода Write() можно записать в файл массив байтов. Это делается следующим образом: void Write(byte[] buf , int offset , int numBytes ) Метод Write() записывает в файл numBytes байтов из массива buf , начиная с элемента buf[offset] .При возникновении во время записи ошибки генерируется исключение типа IOException . Если соответствующий поток не открыт для вывода данных, генерируется исключение типа NotSupportedException . Возможны и другие исключения. Вероятно, вы уже знаете, что при выполнении операции вывода в файл выводимые данные зачастую не записываются немедленно на реальное физическое устройство, а буферизируются операционной системой до тех пор, пока не накопится порция данных достаточного размера, чтобы ее можно было всю сразу переписать на диск. Такой способ выполнения записи данных на диск повышает эффективность системы. Например, дисковые файлы организованы по секторам, которые могут иметь размер от 128 байт. Данные, предназначенные для вывода, обычно буферизируются до тех пор, пока не накопится такой их объем, который позволяет заполнить сразу весь сектор. Но если вы хотите записать данные на физическое устройство вне зависимости от того, полон буфер или нет, вызовите следующий метод Flush() : void Flush() В случае неудачного исхода операции записи генерируется исключение типа IOException Завершив работу с выходным файлом, вы должны его закрыть с помощью метода Close() . Это гарантирует, что любые данные, оставшиеся в дисковом буфере, будут переписаны на диск. Поэтому перед закрытием файла нет необходимости специально вызывать метод Flush() Рассмотрим простой пример записи данных в файл. Глава 14. Использование средств ввода-вывода 387 // Запись данных в файл. using System; using System.IO; class WriteToFile { public static void Main(string[] args) { FileStream fout; // Открываем выходной файл. try { fout = new FileStream("test.txt", FileMode.Create); } catch(IOException exc) { Console.WriteLine( exc.Message + "\nОшибка при открытии выходного файла."); return; } // Записываем в файл алфавит. try { for(char c = 'A'; c <= 'Z'; c++) fout.WriteByte((byte) c); } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при записи в файл. "); } fout.Close(); } } Эта программа сначала открывает для вывода файл с именем test.txt . Затем в этот файл записывается алфавит английского языка, после чего файл закрывается. Обратите внимание на то, как обрабатываются возможные ошибки с помощью блоков try/catch После выполнения этой программы файл test.txt будет иметь такое содержимое: ABCDEFGHIJKLMNOPQRSTUVWXYZ Использование класса FileStream для копирования файла Одно из достоинств байтового ввода-вывода с использованием класса FileStream заключается в том, что этот класс можно использовать для всех типов файлов, а не только текстовых. Например, следующая программа копирует файл любого типа, включая выполняемые файлы. Имена исходного и приемного файлов указываются в командной строке. /* Копирование файла. Для использования этой программы укажите имя исходного и приемного файлов. Например, чтобы скопировать файл FIRST.DAT в файл SECOND.DAT, используйте следующую командную строку: CopyFile FIRST.DAT SECOND.DAT */ 388 Часть I. Язык C# using System; using System.IO; class CopyFile { public static void Main(string[] args) { int i; FileStream fin; FileStream fout; try { // Открываем входной файл. try { fin = new FileStream(args[0], FileMode.Open); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message + "\nВходной файл не найден."); return; } // Открываем выходной файл. try { fout = new FileStream(args[1], FileMode.Create); } catch(IOException exc) { Console.WriteLine( exc.Message + "\nОшибка при открытии выходного файла."); return; } } catch(IndexOutOfRangeException exc) { Console.WriteLine(exc.Message + "\nПрименение: CopyFile ИЗ КУДА"); return; } // Копируем файл. try { do { i = fin.ReadByte(); if(i != -1) fout.WriteByte((byte)i); } while(i != -1); } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при чтении файла. "); } fin.Close(); fout.Close(); } } Глава 14. Использование средств ввода-вывода 389 Файловый ввод-вывод с ориентацией на символы Несмотря на то что байтовая обработка файлов получила широкое распространение, C# также поддерживает символьные потоки. Символьные потоки работают непосредственно с Unicode-символами (это их достоинство). Поэтому, если вы хотите сохранить Unicode-текст, лучше всего выбрать именно символьные потоки. В общем случае, чтобы выполнять файловые операции на символьной основе, поместите объект класса FileStream внутрь объекта класса StreamReader или класса StreamWriter Эти классы автоматически преобразуют байтовый поток в символьный и наоборот. Помните, что на уровне операционной системы файл представляет собой набор байтов. Использование классов StreamReader или StreamWriter не влияет на этот факт. Класс StreamWriter — производный от класса TextWriter , a StreamReader — производный от TextReader . Следовательно, классы StreamWriter и StreamReader имеют доступ к методам и свойствам, определенным их базовыми классами. Использование класса StreamWriter Чтобы создать выходной поток для работы с символами, поместите объект класса Stream (например, FileStream ) в объект класса StreamWriter . В классе StreamWriter определено несколько конструкторов. Самый популярный из них выглядит следующим образом: StreamWriter(Stream stream ) Здесь элемент stream означает имя открытого потока. Этот конструктор генерирует исключение типа ArgumentException , если поток stream не открыт для вывода, и исключение типа ArgumentNullException , если он (поток) имеет null -значение. Созданный объект класса StreamWriter автоматически выполняет преобразование символов в байты. Рассмотрим простую утилиту “клавиатура-диск”, которая считывает строки текста, вводимые с клавиатуры, и записывает их в файл test.txt . Текст считывается до тех пор, пока пользователь не введет слово “стоп”. Здесь используется объект класса FileStream , помещенный в оболочку класса StreamWriter для вывода данных в файл. /* Простая утилита "клавиатура-диск", которая демонстрирует использование класса StreamWriter. */ using System; using System.IO; class KtoD { public static void Main() { string str; FileStream fout; try { fout = new FileStream("test.txt", FileMode.Create); } catch(IOException exc) { 390 Часть I. Язык C# Console.WriteLine(exc.Message + "Не удается открыть файл. "); return; } StreamWriter fstr_out = new StreamWriter(fout); Console.WriteLine( "Введите текст ('стоп' для завершения)."); do { Console.Write(": "); str = Console.ReadLine(); if(str != "стоп") { str = str + "\r\n"; // Добавляем символ // новой строки. try { fstr_out.Write(str); } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при работе с файлом."); return; } } } while(str != "стоп"); fstr_out.Close(); } } Иногда удобнее открывать файл с помощью класса StreamWriter . Для этого используйте один из следующих конструкторов: StreamWriter(string filename ) StreamWriter(string filename , bool appendFlag ) Здесь элемент filename означает имя открываемого файла, причем имя может включать полный путь к файлу. Во второй форме используется параметр appendFlag типа bool : если appendFlag равен значению true , выводимые данные добавляются в конец существующего файла. В противном случае заданный файл перезаписывается. В обоих случаях, если файл не существует, он создается, а при возникновении ошибки ввода- вывода генерируется исключение типа IOException (также возможны и другие исключения). Перед вами новая версия предыдущей утилиты “клавиатура-диск”, в которой для открытия выходного файла используется класс StreamWriter // Открытие файла с использованием класса StreamWriter. using System; using System.IO; class KtoD { public static void Main() { string str; StreamWriter fstr_out; // Открываем файл напрямую, используя // класс StreamWriter. try { fstr_out = new StreamWriter("test.txt"); Глава 14. Использование средств ввода-вывода 391 } catch(IOException exc) { Console.WriteLine(exc.Message + "Не удается открыть файл."); return; } Console.WriteLine( "Введите текст ('стоп' для завершения)."); do { Console.Write(": "); str = Console.ReadLine(); if(str != "стоп") { str = str + "\r\n"; // Добавляем символ // новой строки. try { fstr_out.Write(str); } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при работе с файлом. "); return; } } } while(str != "стоп"); fstr_out.Close(); } } Использование класса StreamReader Чтобы создать входной поток с ориентацией на обработку символов, поместите байтовый поток в класс-оболочку StreamReader . В классе StreamReader определено несколько конструкторов. Чаще всего используется следующий конструктор: StreamReader(Stream stream ) Здесь элемент stream означает имя открытого потока. Этот конструктор генерирует исключение типа ArgumentNullException , если поток stream имеет null -значение, и исключение типа ArgumentException , если поток stream не открыт для ввода. После создания объект класса StreamReader автоматически преобразует байты в символы. Следующая программа создает простую утилиту “клавиатура-диск”, которая считывает текстовый файл test.txt и отображает его содержимое на экране. Таким образом, эта программа представляет собой дополнение к утилите, представленной в предыдущем разделе. /* Простая утилита "клавиатура-диск", которая демонстрирует использование класса FileReader. */ using System; using System.IO; class DtoS { public static void Main() { FileStream fin; 392 Часть I. Язык C# string s; try { fin = new FileStream("test.txt", FileMode.Open); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message + "Не удается открыть файл."); return; } StreamReader fstr_in = new StreamReader(fin); // Считываем файл построчно. while((s = fstr_in.ReadLine()) != null) { Console.WriteLine(s); } fstr_in.Close(); } } Обратите внимание на то, как определяется конец файла. Если ссылка, возвращаемая методом ReadLine() , равна значению null , значит, конец файла достигнут. Как и в случае класса StreamWriter , иногда проще открыть файл, напрямую используя класс StreamReader . Для этого обратитесь к этому конструктору: StreamReader(string filename ) Здесь элемент filename означает имя открываемого файла, которое может включать полный путь к файлу. Указанный файл должен существовать. В противном случае генерируется исключение типа FileNotFoundException . Если параметр filename равен значению null , генерируется исключение типа ArgumentNullException , а если он представляет собой пустую строку, — исключение типа ArgumentException Перенаправление стандартных потоков Как упоминалось выше, такие стандартные потоки, как Console.In , можно перенаправлять. Безусловно, чаще всего они перенаправляются в какой-нибудь файл. При перенаправлении стандартного потока входные и/или выходные данные автоматически направляются в новый поток. При этом устройства, действующие по умолчанию, игнорируются. Благодаря перенаправлению стандартных потоков программа может считывать команды из дискового файла, создавать системные журналы или даже считывать входные данные с сетевых устройств. Перенаправить стандартный поток можно двумя способами. Во-первых, при выполнении программы из командной строки можно использовать операторы “ < ” и “ > ”, чтобы перенаправить потоки Console.In и/или Console.Out , соответственно. Рассмотрим, например, следующую программу: using System; class Test { public static void Main() { Console.WriteLine("Это тест."); } } Глава 14. Использование средств ввода-вывода 393 При выполнении ее с помощью командной строки Test > log текстовая строка “Это тест.” будет записана в файл log . Входной поток можно перенаправить аналогичным способом. При перенаправлении входного потока важно позаботиться о том, чтобы задаваемый источник входных данных содержал информацию, удовлетворяющую требованиям программы. В противном случае программа зависнет. Операторы перенаправления “ < ” и “ > ” являются частью не языка C#, а операционной системы. Таким образом, если среда поддерживает функцию перенаправления потоков ввода-вывода (как это реализовано в Windows), вы сможете перенаправить стандартные входные и выходные потоки, не внося изменений в программы. Однако существует и второй способ, который позволяет перенаправлять стандартные потоки именно программно. Для этого понадобятся следующие методы SetIn() , SetOut() и SetError() , которые являются членами класса Console : static void SetIn(TextReader input ) static void SetOut(TextWriter output ) static void SetError(TextWriter output ) Таким образом, чтобы перенаправить входной поток, вызовите метод SetIn() , указав в качестве параметра желаемый поток. Вы можете использовать любой входной поток, если он является производным от класса TextReader . Чтобы перенаправить выходной поток в файл, задайте FileStream -объект, помещенный в оболочку StreamWriter -объекта. Пример перенаправления потоков проиллюстрирован следующей программой: // Перенаправление потока Console.Out. using System; using System.IO; class Redirect { public static void Main() { StreamWriter log_out; try { log_out = new StreamWriter("logfile.txt"); } catch(IOException exc) { Console.WriteLine(exc.Message + "Не удается открыть файл."); return; } // Направляем стандартный выходной поток в // системный журнал. Console.SetOut(log_out); Console.WriteLine("Это начало системного журнала."); for(int i=0; i<10; i++) Console.WriteLine(i); Console.WriteLine("Это конец системного журнала."); log_out.Close(); } } 394 Часть I. Язык C# При выполнении этой программы на экране не появится ни одного символа, но файл logfile.txt будет иметь такое содержимое: Это начало системного журнала. 0 1 2 3 4 5 6 7 8 9 Это конец системного журнала. При желании вы можете поэкспериментировать, перенаправляя другие встроенные потоки ввода-вывода. Считывание и запись двоичных данных До сих пор мы считывали и записывали байты или символы, но эти операции ввода- вывода можно выполнять и с другими типами данных. Например, вы могли бы создать файл, содержащий int -, double - или short -значения. Для считывания и записи двоичных значений встроенных C#-типов используйте классы BinaryReader и BinaryWriter . Важно понимать, что эти данные считываются и записываются с использованием внутреннего двоичного формата, а не в текстовой форме, понятной человеку. Класс BinaryWriter Класс BinaryWriter представляет собой оболочку для байтового потока, которая управляет записью двоичных данных. Его наиболее употребительный конструктор имеет следующий вид: BinaryWriter(Stream outputStream ) Здесь элемент outputStream означает поток, в который будут записываться данные. Чтобы записать выходные данные в файл, можно использовать для этого параметра объект, созданный классом FileStream . Если поток outputStream имеет null - значение, генерируется исключение типа ArgumentNullException , а если поток outputStream не открыт для записи, — исключение типа ArgumentException В классе BinaryWriter определены методы, способные записывать значения всех встроенных C#-типов (некоторые из них перечислены в табл. 14.5). Обратите внимание: значение типа string записывается с использованием внутреннего формата, который включает спецификатор длины. В классе BinaryWriter также определены стандартные методы Close() и Flush() , работа которых описана выше. Таблица 14.5. Методы вывода информации, определенные в классе BinaryWriter Метод Описание void Write(sbyte val ) Записывает byte -значение (со знаком) void Write(byte val ) Записывает byte -значение (без знака) Глава 14. Использование средств ввода-вывода 395 Окончание табл. 14.5 Метод Описание void Write(byte[] buf ) Записывает массив byte -значений void Write(short val ) Записывает целочисленное значение типа short (короткое целое) void Write(ushort val ) Записывает целочисленное ushort -значение (короткое целое без знака) void Write(int val ) Записывает целочисленное значение типа int void Write(uint val ) Записывает целочисленное uint -значение (целое без знака) void Write(long val ) Записывает целочисленное значение типа long (длинное целое) void Write(ulong val ) Записывает целочисленное ulong -значение (длинное целое без знака) void Write(float val ) Записывает float -значение void Write(double val ) Записывает double -значение void Write(char val ) Записывает символ void Write(char[] buf ) Записывает массив символов void Write(string val ) Записывает string -значение с использованием его внутреннего представления, которое включает спецификатор длины Класс BinaryReader Класс BinaryReader представляет собой оболочку для байтового потока, которая управляет чтением двоичных данных. Его наиболее употребительный конструктор имеет такой вид: BinaryReader(Stream, inputStream ) Здесь элемент inputStream означает поток, из которого считываются данные. Чтобы выполнить чтение из файла, можно использовать для этого параметра объект, созданный классом FileStream , Если поток inputStream имеет null -значение, генерируется исключение типа ArgumentNullException , а если поток inputStream не открыт для чтения,— исключение типа ArgumentException В классе BinaryReader предусмотрены методы для считывания всех простых C#- типов. Наиболее употребимые из них показаны в табл. 14.6. Обратите внимание на то, что метод ReadString() считывает строку, которая хранится с использованием внутреннего формата, включающего спецификатор длины. При обнаружении конца потока все эти методы генерируют исключение типа EndOfStreamException , а при возникновении ошибки — исключение типа IOException . В классе BinaryReader также определены следующие версии метода Read() : Метод Описание int Read() Возвращает целочисленное представление следующего доступного символа из вызывающего входного потока. При обнаружении конца файла возвращает значение -1 int Read( byte[] buf , int offset , int num ) Делает попытку прочитать num байтов в массив buf , начиная с элемента buf[offset] , и возвращает количество успешно считанных байтов int Read( char[] buf , int offset , int num ) Делает попытку прочитать num символов в массив buf , начиная с элемента buf[offset] , и возвращает количество успешно считанных символов В случае неудачного исхода операции чтения эти методы генерируют исключение типа IOException 396 Часть I. Язык C# В классе BinaryReader также определен стандартный метод Close() Таблица 14.6. Методы ввода данных, определенные в классе BinaryReader Метод Описание bool ReadBoolean() Считывает bool -значение byte ReadByte() Считывает byte -значение sbyte ReadSByte() Считывает sbyte -значение byte[] ReadBytes( int num ) Считывает num байтов и возвращает их в виде массива char ReadChar() Считывает char -значение char[] ReadChars( int num ) Считывает num символов и возвращает их в виде массива double ReadDouble() Считывает double -значение float ReadSingle() Считывает float -значение short ReadInt16() Считывает short -значение int ReadInt32() Считывает int -значение long ReadInt64() Считывает long -значение ushort ReadUInt16() Считывает ushort -значение uint ReadUInt32() Считывает uint -значение ulong ReadUInt64() Считывает ulong -значение string ReadString() Считывает string -значение, представленное во внутреннем двоичном формате, который включает спецификатор длины. Этот метод следует использовать для считывания строки, которая была записана с помощью объекта класса BinaryWriter Демонстрация использования двоичного ввода-вывода Рассмотрим программу, которая иллюстрирует использование классов BinaryReader и BinaryWriter . Она записывает в файл данные различных типов, а затем считывает их. // Запись в файл двоичных данных с последующим //их считыванием. using System; using System.IO; class RWData { public static void Main() { BinaryWriter dataOut; BinaryReader dataIn; int i = 10; double d = 1023.56; bool b = true; try { dataOut = new BinaryWriter(new FileStream("testdata", FileMode.Create)); } catch(IOException exc) { Console.WriteLine(exc.Message + "\nНе удается открыть файл."); Глава 14. Использование средств ввода-вывода 397 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); } catch(IOException exc) { Console.WriteLine(exc.Message + "\nОшибка при записи."); } dataOut.Close(); Console.WriteLine(); // Теперь попробуем прочитать эти данные. try { dataIn = new BinaryReader(new FileStream("testdata", FileMode.Open)); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message + "\nНе удается открыть файл."); 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); } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при считывании."); } dataIn.Close(); } } 398 Часть I. Язык C# При выполнении этой программы были получены следующие результаты: Запись 10 Запись 1023,56 Запись True Запись 90,28 Считывание 10 Считывание 1023,56 Считывание True Считывание 90,28 Если вы попробуете просмотреть содержимое файла testdata , созданного этой программой, то увидите, что в нем содержатся двоичные данные, а не понятный для человека текст. А вот более практичный пример, который демонстрирует возможности C#-средств двоичного ввода-вывода. Следующая программа реализует очень простую программу инвентаризации. Для каждого элемента описи программа хранит соответствующее наименование, имеющееся в наличии количество и стоимость. Программа предлагает пользователю ввести наименование элемента описи, а затем выполняет поиск в базе данных. Если элемент найден, на экране отображается соответствующая информация. /* Использование классов BinaryReader и BinaryWriter для реализации простой программы инвентаризации. */ using System; using System.IO; class Inventory { public static void Main() { BinaryWriter dataOut; BinaryReader dataIn; string item; // Наименование элемента. int onhand; // Количество, имеющееся в наличии. double cost; // Цена. try { dataOut = new BinaryWriter(new FileStream("inventory.dat", FileMode.Create)); } catch(IOException exc) { Console.WriteLine(exc.Message + "\nНе удается открыть файл."); return; } // Записываем некоторые инвентаризационные данные // в файл. try { dataOut.Write("Молотки"); dataOut.Write(10); dataOut.Write(3.95); dataOut.Write("Отвертки"); dataOut.Write(18); dataOut.Write(1.50); Глава 14. Использование средств ввода-вывода 399 dataOut.Write("Плоскогубцы"); dataOut.Write(5); dataOut.Write(4.95); dataOut.Write("Пилы"); dataOut.Write(8); dataOut.Write(8.95); } catch(IOException exc) { Console.WriteLine(exc.Message + "\nОшибка при записи."); } dataOut.Close(); Console.WriteLine(); // Теперь откроем файл инвентаризации // для чтения информации. try { dataIn = new BinaryReader(new FileStream("inventory.dat", FileMode.Open)); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message + "\nНе удается открыть файл."); return; } // Поиск элемента, введенного пользователем. Console.Write("Введите наименование для поиска: "); string what = Console.ReadLine(); Console.WriteLine(); try { for(;;) { // Считываем запись из базы данных. item = dataIn.ReadString(); onhand = dataIn.ReadInt32(); cost = dataIn.ReadDouble(); /* Если элемент в базе данных совпадает с элементом из запроса, отображаем найденную информацию. */ if(item.CompareTo(what) == 0) { Console.WriteLine(item + ": " + onhand + " штук в наличии. " + "Цена: {0:C} за каждую единицу.", cost); Console.WriteLine( "Общая стоимость по наименованию <{0}>: {1:С}.", item, cost * onhand); break; } } } catch(EndOfStreamException) { Console.WriteLine("Элемент не найден."); 400 Часть I. Язык C# } catch(IOException exc) { Console.WriteLine(exc.Message + "Ошибка при чтении."); } dataIn.Close(); } } Вот результаты выполнения этой программы: Введите наименование для поиска: Отвертки Отвертки; 18 штук в наличии. Цена: $1.50 за каждую единицу. Общая стоимость по наименованию <Отвертки>: $27.00. В этой программе стоит обратить внимание на то, как хранится информация о наличии товаров на складе, а именно — на двоичный формат хранения данных. Следовательно, количество товаров, имеющихся в наличии, и их стоимость хранятся с использованием двоичного формата, а не в текстовом виде, удобном для восприятия человеком. Это позволяет выполнять вычисления над числовыми данными, не делая дополнительных преобразований. Хотелось бы также обратить ваше внимание на то, как обнаруживается здесь конец файла. Поскольку при достижении конца потока методы ввода двоичной информации генерируют исключение типа EndOfStreamException , эта программа просто считывает содержимое файла до тех пор, пока либо не найдет нужный элемент, либо не сгенерируется это исключение. Таким образом, для обнаружения конца файла в данном случае специального механизма не требуется. Файлы с произвольным доступом До сих пор мы использовали последовательные файлы, т.е. файлы, доступ к содержимому которых организован строго линейно, байт за байтом. Но в C# также возможен доступ к файлу, осуществляющийся случайным образом. В этом случае необходимо использовать метод Seek() , определенный в классе FileStream . Этот метод позволяет установить индикатор позиции (или указатель позиции) в любое место файла. Заголовочная информация о методе Seek() имеет следующий вид: long Seek(long newPos , SeekOrigin origin ) Здесь элемент newPos означает новую позицию, выраженную в байтах, файлового указателя относительно позиции, заданной элементом origin . Элемент origin может принимать одно из значений, определенных перечислением SeekOrigin Значение Описание SeekOrigin.Begin Поиск от начала файла SeekOrigin.Current Поиск от текущей позиции SeekOrigin.End Поиск от конца файла После обращению к методу Seek() следующая операция чтения или записи данных будет выполняться на новой позиции в файле. Если при выполнении поиска возникнет какая-либо ошибка, генерируется исключение типа IOException . Если базовый поток не поддерживает функцию запроса нужной позиции, генерируется исключение типа NotSupportedException Глава 14. Использование средств ввода-вывода 401 Рассмотрим пример, который демонстрирует выполнение операций ввода-вывода с произвольным доступом. Следующая программа записывает в файл алфавит прописными буквами, а затем беспорядочно считывает его. // Демонстрация произвольного доступа к файлу. using System; using System.IO; class RandomAccessDemo { public static void Main() { FileStream f; char ch; try { f = new FileStream("random.dat", FileMode.Create); } catch(IOException exc) { Console.WriteLine(exc.Message); return; } // Записываем в файл алфавит. for(int i=0; i < 26; i++) { try { f.WriteByte( (byte) ('A'+i) ); } catch(IOException exc) { Console.WriteLine(exc.Message); return; } } try { // Теперь считываем отдельные значения. 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 + " "); } } 402 Часть I. Язык C# catch(IOException exc) { Console.WriteLine(exc.Message); } Console.WriteLine(); f.Close(); } } При выполнении этой программы получены такие результаты: Первое значение равно А Второе значение равно В Пятое значение равно Е Выборка значений через одно: A C E G I K M O Q S U W Y Использование класса MemoryStream Не всегда удобно выполнять операции ввода-вывода непосредственно с помощью физического устройства. Иногда полезно считывать входные данные из массива или записывать их в массив. В этом случае стоит воспользоваться классом MemoryStream Класс MemoryStream — это реализация класса Stream , в которой для операций ввода- вывода используются массивы байтов. Вот как выглядит конструктор этого класса: MemoryStream(byte[] buf ) Здесь элемент buf — это массив байтов, который предполагается использовать в операциях ввода-вывода в качестве источника и/или приемника информации. В поток, создаваемый этим конструктором, можно записывать данные или считывать их в него. Этот поток поддерживает метод Seek() . Перед использованием этого конструктора необходимо позаботиться о достаточном размере массива buf , чтобы он позволил сохранить все направляемые в него данные. Вот пример программы, которая демонстрирует использование класса MemoryStream : // Демонстрация использования класса MemoryStream. using System; using System.IO; class MemStrDemo { public static void Main() { byte[] storage = new byte[255]; // Создаем поток с ориентацией на память. MemoryStream memstrm = new MemoryStream(storage); // Помещаем объект memstrm в оболочки StreamWriter //и StreamReader. StreamWriter memwtr = new StreamWriter(memstrm); StreamReader memrdr = new StreamReader(memstrm); // Записываем данные в память с помощью // объекта memwtr. for(int i=0; i < 10; i++) Глава 14. Использование средств ввода-вывода 403 memwtr.WriteLine("'byte[" + i + "]: " + i); // Ставим в конце точку. memwtr.Write('.'); memwtr.Flush(); Console.WriteLine( "Считываем данные прямо из массива 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.CompareTo(".") == 0) break; Console.WriteLine(str); } } } Вот как выглядят результаты выполнения этой программы Считываем данные прямо из массива 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 404 Часть I. Язык C# В этой программе создается байтовый массив storage . Этот массив затем используется в качестве базовой области памяти для объекта memstrm класса MemoryStream . На основе объекта memstrm создаются объект класса StreamReader с именем memrdr и объект класса StreamWriter с именем memwtr . Через объект memwtr данные записываются в поток, ориентированный на конкретную область памяти. Обратите внимание на то, что после записи выходных данных для объекта memwtr вызывается метод flush() . Тем самым гарантируется, что содержимое буфера, связанного с потоком memwtr , реально перепишется в базовый массив. Затем содержимое этого байтового массива отображается “вручную”, т.е. с помощью цикла foreach . После этого посредством метода Seek() указатель позиции устанавливается в начало потока, и его содержимое считывается с использованием объекта memrdr Потоки, ориентированные на память, весьма полезны в программировании. Например, можно заблаговременно составить выходные данные и хранить их в массиве до тех пор, пока в них не отпадет необходимость. Такой подход особенно полезен в программировании для такой GUI-среды, как Windows. Можно также перенаправить стандартный поток для считывания данных из массива. Это полезно, например, при вводе тестовой информации в программу. Использование классов StringReader и StringWriter В некоторых приложениях при выполнении операций ввода-вывода, ориентированных на использование памяти в качестве базовой области хранения данных, проще работать не с байтовыми ( byte -) массивами, а со строковыми ( string -). В этом случае используйте классы StringReader и StringWriter . Класс StringReader наследует класс TextReader , а класс StringWriter — класс TextWriter Следовательно, эти потоки имеют доступ к методам, определенным в этих классах. Например, вы можете вызывать метод ReadLine() для объекта класса StringReader и метод WriteLine() для объекта класса StringWriter Конструктор класса StringReader имеет следующий вид: StringReader(string str ) Здесь параметр str представляет собой строку, из которой должны считываться данные. В классе StringWriter определено несколько конструкторов. Мы будем использовать такой: StringWriter() Этот конструктор создает “записывающий” механизм, который помещает выходные данные в строку. Эта строка автоматически создается объектом класса StringWriter Содержимое строки можно получить, вызвав метод ToString() Рассмотрим пример использования классов StringReader и StringWriter // Демонстрация использования классов StringReader // и StringWriter. using System; using System.IO; class StrRdrDemo { public static void Main() { // Создаем объект класса StringWriter. Глава 14. Использование средств ввода-вывода 405 StringWriter strwtr = new StringWriter(); // Записываем данные в StringWriter-объект. for(int i=0; i < 10; i++) strwtr.WriteLine("Значение i равно: " + i); // Создаем объект класса StringReader. StringReader strrdr = new StringReader( strwtr.ToString() ); // Теперь считываем данные из StringReader-объекта. string str = strrdr.ReadLine(); while(str != null) { str = strrdr.ReadLine(); Console.WriteLine(str); } } } Результаты выполнения этой программы имеют такой вид: Значение i равно: 1 Значение i равно: 2 Значение i равно: 3 Значение i равно: 4 Значение i равно: 5 Значение i равно: 6 Значение i равно: 7 Значение i равно: 8 Значение i равно: 9 Эта программа сначала создает объект класса StringWriter с именем strwtr и записывает в него данные с помощью метода WriteLine() . Затем создается объект класса StringReader с использованием строки, содержащейся в объекте strwtr , и метода ToString() . Наконец, содержимое строки считывается с помощью метода ReadLine() Преобразование числовых строк во внутреннее представление Прежде чем завершить тему ввода-вывода, рассмотрим метод, который будет весьма полезен программистам при считывании числовых строк. Как вы знаете, C#-метод WriteLine() предоставляет удобный способ вывода данных различных типов (включая числовые значения таких встроенных типов, как int и double ) на консольное устройство. Следовательно, метод WriteLine() автоматически преобразует числовые значения в удобную для восприятия человеком форму. Однако C# не обеспечивает обратную функцию, т.е. метод ввода, который бы считывал и преобразовывал строковые представления числовых значений во внутренний двоичный формат. Например, не существует метода ввода данных, который бы считывал такую строку, как “100”, и автоматически преобразовывал ее в соответствующее двоичное значение, которое можно было бы хранить в int -переменной. Для решения этой задачи понадобится метод, определенный для всех встроенных числовых типов, — Parse() 406 Часть I. Язык C# Приступая к решению этой задачи, необходимо отметить такой важный факт. Все встроенные C#-типы (например, int и double ) в действительности являются лишь псевдонимами (т.е. другими именами) для структур, определенных в среде .NET Framework. Компания Microsoft заявляет, что понятия C#-типа и .NET-типа структуры неразличимы. Первое — просто еще одно имя для другого. Поскольку 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 . Для этих структур определен широкий диапазон методов, которые способствуют полной интеграции типов значений в C#-иерархию объектов. В качестве дополнительного “вознаграждения” эти числовые структуры также определяют статические методы, которые преобразуют числовую строку в соответствующий двоичный эквивалент. Эти методы преобразования представлены в следующей таблице. Каждый метод возвращает двоичное значение, которое соответствует строке. Структура Метод преобразования Decimal static decimal Parse(string str ) Double static double Parse(string str ) Single static float Parse(string str ) Int64 static long Parse(string str ) Int32 static int Parse(string str ) Int16 static short Parse(string str ) UInt64 static ulong Parse(string str ) UInt32 static uint Parse(string str ) UInt16 static ushort Parse(string str ) Byte static byte Parse(string str ) SByte static sbyte Parse(string str ) Методы Parse() генерируют исключение типа FormatException , если параметр str не содержит числа, допустимого для типа вызывающего объекта. Если параметр str имеет null -значение, генерируется исключение типа ArgumentNullException , a Глава 14. Использование средств ввода-вывода 407 если значение параметра str превышает диапазон, допустимый для типа вызывающего объекта, — исключение типа OverflowException Методы синтаксического анализа позволяют легко преобразовать числовое значение, прочитанное в виде строки с клавиатуры или текстового файла, в соответствующий внутренний формат. Например, следующая программа вычисляет среднее арифметическое от чисел, введенных пользователем в виде списка. Сначала пользователю предлагается ввести количество усредняемых чисел, а затем программа считывает эти числа с помощью метода ReadLine() и с помощью метода Int32.Parse() преобразует строки в целочисленное значение. Затем она вводит значения, используя метод Double. Parse() для преобразования строк в их double -эквиваленты. // Эта программа усредняет список чисел, // введенных пользователем. using System; using System.IO; class AvgNums { public 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); n = 0; } catch(OverflowException exc) { Console.WriteLine(exc.Message); n = 0; } 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); } } 408 Часть I. Язык C# Вот как могут выглядеть результаты выполнения этой программы Сколько чисел вы собираетесь ввести: 5 Введите 5 чисел. : 1.1 : 2.2 : 3.3 : 4.4 : 5.5 Среднее равно 3.3 И еще. Вы должны использовать надлежащий метод анализа для типа значения, которое вы пытаетесь преобразовать. Например, попытка использовать метод Int32.Parse() для строки, содержащей значение с плавающей точкой, желаемого результата не даст. |