Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 14 Использование средств ввода-вывода 376 Часть I. Язык C# самого начала книги мы использовали часть C#-системы ввода-вывода — метод Console.WriteLine() , но не давали подробных пояснений по этому поводу. Поскольку C#-система ввода-вывода построена на иерархии классов, то ее теорию и детали невозможно освоить, не рассмотрев сначала классы, наследование и исключения. Теперь настало время для подробного изучения C#-средств ввода-вывода. Поскольку в C# используется система ввода-вывода и классы, определенные средой .NET Framework, в эту тему включено рассмотрение в общих чертах системы ввода-вывода .NET-среды. В этой главе рассматриваются средства как консольного, так и файлового ввода- вывода. Необходимо сразу отметить, что C#-система ввода-вывода — довольно обширная тема, и здесь описаны лишь самые важные и часто применяемые средства. Организация C#-системы ввода-вывода C#-программы выполняют операции ввода-вывода посредством потоков. Поток (stream) — это абстракция, которая либо синтезирует информацию, либо потребляет ее. Поток связывается с физическим устройством с помощью C#-системы ввода-вывода. Характер поведения всех потоков одинаков, несмотря на различные физические устройства, с которыми они связываются. Следовательно, классы и методы ввода-вывода можно применить ко многим типам устройств. Например, методы, используемые для записи данных на консольное устройство, также можно использовать для записи в дисковый файл. Байтовые и символьные потоки На самом низком уровне все C#-системы ввода-вывода оперируют байтами. И это логично, поскольку многие устройства при выполнении операций ввода-вывода ориентированы на байты. Однако для человека привычнее оперировать символами. Вспомните, что в C# char — это 16-разрядный тип, a byte — 8-разрядный. Если вы используете набор символов ASCII, то в преобразовании типов char и byte нет ничего сложного: достаточно проигнорировать старший байт char -значения. Но такой подход не сработает для остальных Unicode-символов, которым необходимы оба байта. Таким образом, байтовые потоки не вполне подходят для обработки текстового (ориентированного на символы) ввода-вывода. Для решения этой проблемы в C# определен ряд классов, которые преобразуют байтовый поток в символьный, выполняя byte-char - и char- byte -перевод автоматически. Встроенные потоки Тремя встроенными потоками, доступ к которым осуществляется через свойства Console.In , Console.Out и Console.Error , могут пользоваться все программы, работающие в пространстве имен System . Свойство Console.Out относится к стандартному выходному потоку. По умолчанию это консоль. Например, при вызове метода Console.WriteLine() информация автоматически передается в поток Console.Out Свойство Console.In относится к стандартному входному потоку, источником которого по умолчанию является клавиатура. Свойство Console.Error относится к ошибкам в стандартном потоке, источником которого также по умолчанию является консоль. Однако эти потоки могут быть перенаправлены на любое совместимое устройство ввода-вывода. Стандартные потоки являются символьными. Следовательно, эти потоки считывают и записывают символы. С Глава 14. Использование средств ввода-вывода 377 Классы потоков В C# определены как байтовые, так и символьные классы потоков. Однако символьные классы потоков в действительности представляют собой оболочки, которые преобразуют базовый байтовый поток в символьный, причем любое преобразование выполняется автоматически. Таким образом, символьные потоки построены на основе байтовых, несмотря на то, что они логически отделены друг от друга. Все классы потоков определены в пространстве имен System.IO . Чтобы иметь возможность использовать эти классы, в начало программы необходимо включить следующую инструкцию: using System.IO; Для ввода и вывода на консоль не нужно задавать пространство имен System.IO , поскольку класс Console определен в пространстве имен System Класс Stream Центральную часть потоковой C#-системы занимает класс System.IO.Stream Класс Stream представляет байтовый поток и является базовым для всех остальных потоковых классов. Этот класс также абстрактный, т.е. мы не можем создать его объект. В классе Stream определен набор стандартных потоковых операций. Наиболее применяемые методы, определенные в классе Stream , перечислены в табл. 14.1. Таблица 14.1. Некоторые методы класса Stream Метод Описание void Close() Закрывает поток void Flush() Записывает содержимое потока в физическое устройство int ReadByte() Возвращает целочисленное представление следующего доступного байта потока. При обнаружении конца файла возвращает значение -1 int Read( byte[] buf , int offset , int numBytes ) Делает попытку прочитать numBytes байт в массив buf , начиная с элемента buf[offset] , возвращает количество успешно прочитанных байтов long Seek (long offset , SeekOrigin origin ) Устанавливает текущую позицию потока равной указанному значению смещения от заданного начала отсчета void WriteByte (byte b ) Записывает один байт в выходной поток void Write(byte[] buf , int offset , int numBytes ) Записывает поддиапазон размером numBytes байт из массива buf , начиная с элемента buf[offset] В общем случае при возникновении ошибки ввода-вывода методы, представленные в табл. 14.1, генерируют исключение типа IOException . При попытке выполнить некорректную операцию, например, записать данные в поток, предназначенный только для чтения, генерируется исключение типа NotSupportedException Обратите внимание на то, что в классе Stream определены методы, которые считывают и записывают данные. Однако не все потоки поддерживают все эти операции, поскольку возможен вариант, когда поток открывается только для чтения или только для записи. Кроме того, не все потоки поддерживают функцию установки в заданную позицию с помощью метода Seek() . Чтобы определить возможности потока, используйте одно или несколько свойств класса Stream . Они представлены в табл. 14.2. В этой таблице также описаны свойства Length и Position , которые содержат длину и текущую позицию потока, соответственно. 378 Часть I. Язык C# Таблица 14.2. Свойства, определенные в классе Stream Свойство Описание bool CanRead Свойство равно значению true , если из потока можно считывать данные. Это свойство предназначено только для чтения bool CanSeek Свойство равно значению true , если поток поддерживает функцию установки в заданную позицию. Это свойство предназначено только для чтения bool CanWrite Свойство равно значению true , если в поток можно записывать данные. Это свойство предназначено только для чтения long Length Свойство содержит длину потока. Это свойство предназначено только для чтения long Position Свойство представляет текущую позицию потока. Это свойство можно как читать, так и устанавливать Байтовые классы потоков Из класса Stream выведены такие байтовые классы потоков. Класс потока Описание BufferedStream Заключает в оболочку байтовый поток и добавляет буферизацию. Буферизация во многих случаях увеличивает производительность FileStream Байтовый поток, разработанный для файлового ввода-вывода MemoryStream Байтовый поток, который использует память для хранения данных Программист может также вывести собственные потоковые классы. Однако для подавляющего большинства приложений достаточно встроенных потоков. Символьные классы потоков Чтобы создать символьный поток, поместите байтовый поток в одну из символьных потоковых C#-оболочек. В вершине иерархии символьных потоков находятся абстрактные классы TextReader и TextWriter . Класс TextReader предназначен для обработки операций ввода данных, а класс TextWriter — для обработки операций вывода данных. Методы, определенные этими двумя абстрактными классами, доступны для всех их подклассов. Следовательно, они образуют минимальный набор функций ввода-вывода, который будут иметь все символьные потоки. В табл. 14.3 перечислены методы ввода данных, принадлежащие классу TextReader . В случае ошибки эти методы могут генерировать исключение типа IOException . (Некоторые методы могут также генерировать и другие типы исключений.) Особого внимания заслуживает метод ReadLine() , который считывает целую строку текста, возвращая ее в качестве string -значения. Этот метод полезен при считывании входных данных, которые содержат пробелы. Таблица 14.3. Методы ввода данных, определенные в классе TextReader Метод Описание void Close() Закрывает источник ввода данных int Peek() Получает следующий символ из входного потока, но не удаляет его. Возвращает значение -1, если ни один символ не доступен int Read() Возвращает целочисленное представление следующего доступного символа из вызывающего объекта входного потока. При обнаружении конца файла возвращает значение -1 Глава 14. Использование средств ввода-вывода 379 Окончание табл. 14.3 Метод Описание int Read(char[] buf , int offset , int numChars ) Делает попытку прочитать numChars символов в массив buf , начиная с элемента buf[offset] , и возвращает количество успешно прочитанных символов int ReadBlock(char[] buf , int offset , int numChars ) Делает попытку прочитать numChars символов в массив buf , начиная с элемента buf[offset] , и возвращает количество успешно прочитанных символов string ReadLine() Считывает следующую строку текста и возвращает ее как string -значение. При попытке прочитать признак конца файла возвращает null -значение string ReadToEnd() Считывает все символы, оставшиеся в потоке, и возвращает их как string -значение В классе TextWriter определены версии методов Write() и WriteLine() , которые могут выводить данные всех встроенных типов. Приведем, например, только некоторые их перегруженные версии. Метод Описание void Write(int val ) Записывает значение типа int void Write(double val ) Записывает значение типа double void Write(bool val ) Записывает значение типа bool void WriteLine(string val ) Записывает значение типа string с последующим символом новой строки void WriteLine(uint val ) Записывает значение типа uint с последующим символом новой строки void WriteLine(char val ) Записывает символ с последующим символом новой строки Помимо методов Write() и WriteLine() , в классе TextWriter также определены методы Close() и Flush() : virtual void Close() virtual void Flush() Метод Flush() записывает все данные, оставшиеся в выходном буфере, на физический носитель информации. Метод Close() закрывает поток. Из классов TextReader и TextWriter выведен ряд символьно-ориентированных потоковых классов, в том числе и те, что перечислены в следующей таблице. Следовательно, эти потоковые классы используют методы и свойства, определенные в классах TextReader и TextWriter Потоковый класс Описание StreamReader Предназначен для чтения символов из байтового потока. Этот класс является оболочкой для байтового входного потока StreamWriter Предназначен для записи символов в байтовый поток. Этот класс является оболочкой для байтового выходного потока SstringReader Предназначен для чтения символов из строки StringWriter Предназначен для записи символов в строку 380 Часть I. Язык C# Двоичные потоки Помимо байтовых и символьных потоков, в C# определены два двоичных потоковых класса, которые можно использовать для прямого считывания и записи двоичных данных. Эти классы, которые называются BinaryReader и BinaryWriter , будут рассмотрены ниже в этой главе в теме двоичного файлового ввода-вывода. Теперь, когда вы получили общее представление о C#-системе ввода-вывода, можно переходить к детальному изучению этой темы, которую, пожалуй, стоит начать с консольного ввода-вывода. Консольный ввод-вывод данных Консольный ввод-вывод данных реализуется посредством стандартных потоков Console.In , Console.Out и Console.Error . Консольные классы ввода-вывода использовались, начиная с главы 2, поэтому вы уже с ними знакомы. Как вы убедитесь ниже, они обладают и другими возможностями. Прежде всего, важно отметить, что большинство реальных C#-приложений являются не текстовыми, или консольными, а графическими программами или компонентами, которые опираются на оконный интерфейс, предназначенный для взаимодействия с пользователем. Таким образом, часть C#-системы ввода-вывода, которая связана с консольным вводом-выводом данных, не относится к широко используемым средствам. Несмотря на то что текстовые программы — прекрасные учебные примеры коротких утилит и некоторых типов компонентов, они не годятся для большинства реальных приложений. Считывание данных из консольного входного потока Поток Console.In — экземпляр класса TextReader , поэтому для доступа к нему можно использовать методы и свойства, определенные в классе TextReader . Однако обычно используют методы, определенные в классе Console , которые автоматически считывают значение свойства Console.In . В классе Console определено два метода ввода информации: Read() и ReadLine() Метод Read() используется для считывания одного символа. static int Read() Метод Read() возвращает следующий символ, считанный с консоли. Он ожидает, пока пользователь не нажмет какую-нибудь клавишу, а затем возвращает результат. Считанный символ возвращается как значение типа int , которое должно быть приведено к типу char . При возникновении ошибки метод Read() возвращает —1, а в случае неудачного исхода операции генерирует исключение типа IOException . По умолчанию консольный ввод данных буферизован (с ориентацией на строки), поэтому, прежде чем введенный с клавиатуры символ будет послан программе, необходимо нажать клавишу Рассмотрим пример программы, которая считывает символ с клавиатуры с помощью метода Read() // Считывание символа с клавиатуры. using System; class KbIn { public static void Main() { char ch; Глава 14. Использование средств ввода-вывода 381 Console.Write( "Нажмите любую клавишу, а затем -- // char-значения. Console.WriteLine("Вы нажали клавишу: " + ch); } } Вот как могут выглядеть результаты выполнения этой программы: Нажмите любую клавишу, а затем — Вы нажали клавишу: ю Тот факт, что метод Read() буферизирует строки, является порой источником досадных недоразумений. При нажатии клавиши Чтобы прочитать строку символов, используйте метод ReadLine() static string ReadLine() Метод ReadLine() считывает символы до тех пор, пока не будет нажата клавиша . При неудачном завершении метод генерирует исключение типа IOException Рассмотрим программу, которая демонстрирует считывание строки из потока Console.In с помощью метода ReadLine() // Ввод данных с консоли с помощью метода ReadLine(). using System; class ReadString { public static void Main() { string str; Console.WriteLine("Введите несколько символов."); str = Console.ReadLine(); Console.WriteLine("Вы ввели: " + str); } } Вот такие результаты можно получить при выполнении этой программы. Введите несколько символов. Это всего лишь тест. Вы ввели: Это всего лишь тест. Несмотря на то что методы класса Console являются самым простым способом считывания данных из потока Console.In , можно с таким же успехом вызывать методы базового класса TextReader . Например, перепишем предыдущую программу с использованием методов, определенных в классе TextReader // Считывание строки с клавиатуры благодаря // непосредственному использованию свойства Console.In. using System; class ReadChars2 { public static void Main() { string str; 382 Часть I. Язык C# Console.WriteLine("Введите несколько символов."); str = Console.In.ReadLine(); Console.WriteLine("Вы ввели: " + str); } } Обратите внимание на то, что метод ReadLine() теперь напрямую вызывается для потока Console.In . Здесь важно то, что при необходимости доступа к методам, определенным в классе TextReader , который является базовым для объекта Console.In , их можно вызывать так, как показано в этом примере. Запись данных в консольный выходной поток Потоки 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 { public 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); } } } При выполнении этой программы получим следующие результаты: Деление на нуль сгенерирует исключение. Attempted to divide by zero. Иногда неопытные программисты путаются, не зная точно, когда следует использовать поток Console.Error . Если и Console.Out , и Console.Error по умолчанию выводят данные на консоль, то почему существует два различных потока? Ответ прост: стандартные потоки можно перенаправить на другие устройства. Например, поток Console.Error можно перенаправить для записи данных в дисковый файл, а не на экран. Следовательно, и поток ошибок можно перенаправить, например, в системный журнал (log file), не затрагивая при этом консольный вывод. И наоборот, если консольный вывод данных перенаправить в файл, а вывод ошибок — нет, то сообщения |