| « Поставить закладку » « Сделать стартовой » | |||
|
|||
|
Глава 9. Ввод-вывод
Глава №9. Ввод-вывод В прежних
версиях Visual Basic использовались разные средства обработки данных, причем
выбор определялся типом источника данных. Например, обработка данных,
прочитанных из файла на диске, принципиально отличалась от обработки данных,
загруженных из Интернета. Эти времена остались в прошлом. Одной из целей,
поставленных при проектировании .NET Framework, было обеспечение
унифицированного механизма обработки данных, не зависящего от
источника. Центральное
место в этом механизме занимает понятие потока (stream). Термин «поток» в
данном случае происходит от выражения «поток байтов». Собственно, все данные, с
которыми работает компьютер, — числа, текст и графика — сводятся к обычной
последовательности байтов. Таким образом, подход, избранный проектировщиками
.NET Framework, выглядит вполне логично — они разработали абстрактный класс,
выполняющий обобщенные операции с данными. Наличие абстрактного класса упрощает
программирование ввода-вывода в VB .NET и выявляет сходство между разнородными
операциями. Короче говоря, абстрактный класс Stream, помеченный ключевым словом
Mustlnherit, является идеальной базой для построения объектно-ориентированной
иерархии ввода-вывода. При работе с
потоками данных, поступающих с клавиатуры, из памяти, из файла или сетевого
соединения, программисты .NET используют классы, адаптированные для конкретных
источников. Но прежде, чем рассматривать эти типы потоков, необходимо
познакомиться с тем, как организовано локальное хранение данных пользователем.
По этой причине мы начинаем эту главу с описания операций с файлами и
каталогами, а затем рассмотрим существующие разновидности потоков .NET, в том
числе файловые, сетевые и объектные потоки, позволяющие сохранять объекты на
диске, и другие источники данных. Глава
завершается примером использования новых серверных средств RAD (Rapid
Application Development) для написания простого монитора файловой системы.
Программа следит за изменениями в каталогах (такими, как создание и удаление
файлов) и обрабатывает различные события, инициируемые в зависимости от типа
изменений. В предыдущих версиях VB написание подобных программ было сильно
затруднено тем, что программисту приходилось использовать Windows API весьма
нетривиальным образом. И хотя в этой книге мы не сможем сколько-нибудь полно
описать RAD-иногоументарий VB .NET, мы надеемся, что это подстегнет ваше
любопытство и заставит подробнее изучить этот чрезвычайно полезный аспект VB
.NET.
В VB .NET
существуют два класса для работы с каталогами и два класса для работы с
файлами.
Обращение к
функциональным возможностям классов Directory и File происходит при помощи общих
методов. Поскольку методы классов Di rectory и Fi1е являются общими, они могут
вызываться и без предварительного создания экземпляра оператором New. Конечно,
это повышает их эффективность при разовых обращениях к конкретному файлу или
каталогу. Тем не менее при многократном обращении к файлу или каталогу эти
методы становятся менее эффективными. Классы Di rectorylnfo и Filelnfo содержат
обычные методы, поэтому обращение к их членам происходит через конкретные
экземпляры. Другое различие
между этими парами заключается в том, что классы Directory и File являются
производными непосредственно от Object, а классы Directory-Info и FileInfo
объявлены производными от абстрактного (Mustlnherit) класса FileSystemInfo,
содержащего универсальные методы вроде LastAccessTime и FullName. И все же самое
принципиальное различие состоит в другом. Классы Directorylnfo и Filelnfo
гораздо лучше подходят для рекурсивного использования результатов, как было
показано в примере, приведенном в главе 4. Дело в том, что члены классов
Directory и File обычно возвращают строки с описанием каталогов или файлов,
тогда как члены классов Di rectorylnfo и Filelnfo обычно возвращают экземпляры
своих классов. Как было показано в главе 4, эта особенность упрощает написание
рекурсивных программ.
Поскольку
существование данных, к которым вы обращаетесь, не гарантировано, обращения к
файлам или каталогам часто заключаются в блоки Try-Catch. Впрочем, на эти классы
распространяется одно из основных правил при работе с исключениями: не
используйте исключения там, где можно ограничиться простой проверкой. Например,
в обычных условиях незачем перехватывать исключения Di rectoryNotFoundExcepti on
— проще предварительно вызвать метод Exists и убедиться в том, что каталог
существует. Ниже перечислены основные исключения, встречающиеся при операциях с
файлами и каталогами. Иерархию возглавляет базовый класс
IOException: IOException >DirectoryNotFoundException >
EndOfStreamException >FileLoadException >FileNotFoundException Прежде чем
рассматривать операции с каталогами и файлами, следует познакомиться с классом
Path. Этот класс содержит несколько общих методов, предназначенных для обработки
уточненных имен файлов [Любопытная подробность: в описании этого класса,
приведением в документации VB .NET, упоминаются некоторые аспекты
кросс-платформенных операций. В частности, упоминается о различиях между
символом «/» и разделителем каталогов «», используемым в системах семейства
UNIX (в том числе и в системе BSD, для которой Microsoft анонсировала поддержку
CLR).]. Сетевые имена файлов устроены несколько сложнее локальных имен,
поэтому методы класса Path приносят несомненную пользу (кстати говоря, анализ
даже локальных имен — занятие на любителя). Основные члены класса Path
перечислены в табл. 9.1. Таблица
9.1. Важнейшие члены класса Path
Большинство
методов класса Directory идентифицирует каталоги при помощи возвращаемых строк.
Поскольку все члены класса объявлены общими, при обращении к ним не обязательно
указывать конкретный экземпляр. Пример: System.IO.Directory.GetCurrentDirectory() Эта команда
возвращает строку с описанием текущего каталога. Метод
GetDirectories(pathString) возвращает массив строк с описанием подкаталогов
каталога, заданного параметром pathString. Описание интерпретируется либо как
путь, заданный относительно каталога текущего приложения, либо как путь в схеме
UNC (Universal Naming Convention). Следующая программа выводит имя текущего
каталога и имена всех его подкаталогов. Imports System.IO
Module Modulel Sub
Main() Dim curDir.nextDir
As String Try curDir
=Directory.GetCurrentDirectory () Console.WriteLine(curDir) For Each nextDir In
Directory.GetDirectories(curDir) Console.WriteLine(nextDir) Next Catch ioe As
IOException Console.WriteLine("eeeks -i/o problems!" &
ioe.message) Catch e As
Exception Console.Write(e.stacktrace) Finally Console.ReadLine() End Try End Sub End
Module Если ваши потребности не ограничиваются простым выводом имен каталогов, лучше воспользоваться классом DirectoryInfo. Более подробное описание этого класса приводится ниже. Помимо передачи
строки с описанием каталога методу GetDirectories можно передать шаблон с
метасимволами, используемыми в DOS [«?» обозначает один символ, а «*» —
несколько символов.]. Важнейшие методы класса Di rectory перечислены в табл.
9.2. Во всех случаях параметры передаются по значению (с ключевым словом
ByVal). Таблица
9.2. Важнейшие методы класса Directory
Класс File, как
и класс Directory, состоит из общих методов, которым при вызове обычно
передается имя файла. Эти методы применяктея при копировании, удалении и перемещении файлов. Основные
методы класса File перечислены в табл. 9.3. Обратите внимание,— все параметры
передаются по значению (в таблице отсутствуют методы класса File,
предназначенные для работы с потоками данных, — они будут рассмотрены
ниже). Таблица
9.3. Основные методы класса File
Операции с
атрибутами файлов и каталогов выполняются достаточно часто, поэтому в .NET
Framework был включен удобный класс FileAttri bute. Вероятно, правильнее было бы
назвать его FileDi rectoryAttri bute, поскольку все атрибуты относятся не только
к файлам, но и к каталогам. Значения
перечисляемого типа обычно объединяются поразрядными операциями, чтобы избежать
всевозможных ошибок в программе. Не используйте команды следующего
вида: If
File.GetAttributes("c:foo.txt") = FileAttributes.Readonly Then... В проверяемом
условии не учитывается тот факт, что у файла могут быть установлены и другие
атрибуты. Правильная команда должна выглядеть так: If File.GetAttributes("c:foo.txt") And FileAttributes.Readonly _ =
FileAttributes.Readonly Then... При
необходимости атрибуты объединяются оператором Оr. Пример: File.SetAttributes(
"с: foo.txt". Not
(FileAttributes.Archive) Or FileAttributes.Hidden) Команда
назначает атрибуты C:foo.txt таким образом, что файл становится скрытым
(Hidden), а архивный бит (Archive) сбрасывается. Ниже перечислены важнейшие
значения этого перечисляемого типа: Archive Compressed Di
rectory Encrypted Hidden Normal (атрибуты не
установлены) Readonly System Классы DirectoryInfo и FileInfo В отличие от
обобщенных классов Directory и Filе классы Directory Info и FileInfо
инкапсулируют конкретные (или потенциально существующие) каталоги и файлы. Чтобы
использовать их, необходимо предварительно создать экземпляр класса. Под
потенциальным существованием мы имеем в виду, что объект Di rectorylnfo или Fi
lelnfo может быть создан даже в том случае, если файл или каталог с заданным
именем еще не существует и создается при последующем вызове метода
Create. Как правило, при
создании экземпляров этих классов при вызове конструктора указывается имя
каталога или файла. Пример: Dim myDirectory As
Directorylnfo myDirectory = New
Directorylnfo("C:Test Directory") Текущий каталог
обозначается символом «.»: Dim currentDir As
New Directorylnfo(".") После создания
объекта Directorylnfo можно запросить различные сведения о соответствующем
каталоге — например, время создания: MsgBox(myDirectory.GreatienTime) Как упоминалось
выше, одна из самых замечательных особенностей этих классов заключается в том,
что их члены возвращают объекты, а не строки. Например, в следующей программе
вызов GetFiles в выделенной строке возвращает коллекцию объектов Filelnfo, что
позволяет при необходимости вызвать методы этих объектов. Imports
System.IO Module Modulel Sub
Main() Dim myDi rectory As
Directorylnfo Try myDirectory =New
DirectoryInfo("C:Test Directory") Dim aFile As File
Info For Each aFile In
myDirectory.GetFiles Consol e. WriteLi ne( "The fi1e named " & aFile. Full Name & _ "has length " &
aFile.Length) Next Catch e As Exception MsgBox("eeks -an
exception " & e.StackTrace) Finally Console.WriteLine("Press enter to end") Console.ReadLine() End Try End Sub End
Module Рекурсивный просмотр дерева каталогов Класс
Directorylnfo удобен тем, что на его основе легко строятся обобщенные процедуры
для рекурсивного перебора дерева каталогов. Как было показано в главе 4, при
этом удобно использовать вспомогательную процедуру, которая, в свою очередь,
вызывает другую процедуру для работы с файлами заданного каталога. Ниже
приведена одна из возможных реализаций этого рекурсивного
процесса: Option Strict On
Imports System.IO Module Modulel SubMain() Dim nameOfDirectory As String ="C:" Dim myDirectory As DirectoryInfo myDirectory = New DirectoryInfo(nameOfDirectory) WorkWithDirectory(myDirectory) End
Sub Public Sub WorkWithDirectory(ByVal aDir As Directorylnfo) Dim nextDir As Directorylnfo WorkWithFilesInDir(aDir) For Each nextDir In
aDir.GetDirectories WorkWithDirectory(nextDir) Next End
Sub Public Sub WbrkWithFilesInDir(ByVal aDir As Directorylnfo) Dim aFile As Filelnfo For Each aFile In aDir.GetFiles() ' Выполнить операцию с файлом. ' В нашем примере просто выводится уточненное имя. Consolе.WriteLine(aFi1e.Ful1 Name) Next End Sub End
Module Следующий, более
реалистичный пример активизирует форму, показанную на рис. 9.1. Программа
заносит все скрытые файлы заданного каталога в список и продолжает рекурсивную
обработку дерева каталогов. Курсор мыши заменяется изображением песочных часов;
по этому признаку пользователь узнает о том, что программа выполняет какую-то
длительную операцию.
Private Sub Buttonl_Click(ByVal sender As System.Object,_ ByVal e As System.EventArgs) Handles Buttonl.Click 'Заменить курсор изображением песочных часов Me.Cursor =
Cursors.WaitCursor ListBoxl. Items. Clear() WorkWithDirectory(New Directorylnfo(TextBoxl.Text)) Me.Cursor = Cursors.Default End
Sub Public Sub WorkWithDirectory(ByVal aDir As Directorylnfo) Dim nextDir As
Directorylnfo Try WorkWithFilesInDir(aDir) For Each nextDir In
aDir.GetDirectories WorkWithDirectory(nextDi r) Next Catch e As
Exception MsgBox(e.message SvbCrLf Se.StackTrace) End Try End
Sub Public Sub WorkWithFilesInDir(ByVal aDir As Directorylnfo) Dim aFile As Filelnfo For Each aFile In aDir.GetFiles() If aFile.Attributes
And _ FileAttributes.Hidden = FileAttributes.Hidden Then ListBoxl. Items. Add( "FOUND hidden filenamed " & aFile. FullName) End If Next End
Sub
Рис.
9.1. Форма для рекурсивного просмотра каталогов
Важнейшие члены классов FileSystemInfo, FileInfo и
DirectoryInfo Класс
FileSystemlnfo является базовым для классов Directorylnfo и Filelnfo и содержит
большую часть их общей функциональности. Перед нами хороший пример тех
возможностей, которые открываются при использовании абстрактных базовых классов.
В классе Directory Info существует метод GetFileSystemlnfos, который возвращает
массив объектов FileSystemlnfо, представляющих файлы и подкаталоги заданного
каталога. Такое становится возможным только благодаря существованию класса
FileSystemlnfo. Важнейшие члены базового класса FileSystemlnf о перечислены в
табл. 9.4. Таблица
9.4. Члены базового класса FileSystemInfo
В табл. 9.5 и
9.6 перечислены важнейшие методы класса DirectoryInfo и методы класса Filelnfo,
не имеющие непосредственного отношения к потокам (эта тема будет рассматриваться
позже). Таблица
9.5. Основные методы класса DirectoryInfo
Таблица
9.6. Члены класса Filelnfo, не возвращающие
потоков
Как упоминалось
во вступительной части, одной из целей проектирования класса System. I0.Stream
было абстрагирование примитивных операций при работе с потоками байтов. В соответствий с этой
концепцией каждая конкретная реализация класса Stream должна предоставить свои
версии следующих методов:
Впрочем, этим
возможности не ограничиваются. Кроме простого перемещения от первого байта к
последнему реализация класса Stream может поддерживать и другие способы —
например, перемещение в обратном направлении или непосредственный переход к
заданной позиции в потоке. Такое возможно для файловых потоков, но не имеет
смысла (а следовательно, и не реализуется) для потоков, основанных на сетевых
соединениях. Свойство CanSeek позволяет узнать, поддерживает ли поток
произвольный доступ. Если свойство равно True, значит, в производном классе
поддерживаются реализации методов Seek и SetLength, а также свойств Position и
Length.
В табл. 9.7
перечислены основные методы абстрактного класса Stream, смысл которых должен
сохраниться и в производных классах. Таблица
9.7. Основные методы класса Stream
Все классы
иерархии Stream поддерживают метод Close, освобождающий удерживаемые ресурсы
операционной системы (например, файловые манипуляторы или сетевые соединения),
поэтому практически во всех программах, работающих с потоками, рекомендуется
закрывать поток в блоке Try-Catch-Final 1у. Учтите, что вызов Close в секции
Finally требует предварительной проверки, поскольку этот метод вызывается лишь
для существующих объектов потоков, созданных успешным вызовом конструктора.
Проверка перед вызовом Close в секции Final ly выглядит примерно
так: Finally If Not (myFileStream Is Nothing) Then myFileStream.Close() End
Try Рассмотрите и
такую возможность, как реализация IDisposable в классах, выполняющих операции с
файлами, и закрытие всех открытых потоков методом Dispose. Основные классы, производные от Stream, перечислены в табл. 9.8. Таблица
9.8. Основные классы, производные от Stream
Начнем с
рассмотрения команды, часто встречающейся при работе с файловыми
потоками: Dim myFileStream As
New FileStream("MyFile.txt". FileMode.OpenOrCreate,
FileAccess.Write) Как видно из
приведенного фрагмента, эта версия конструктора FileStream получает имя файла
(заданное по отношению к текущему каталогу, если не указано полное имя) и два
параметра, значения которых относятся к перечисляемым типам FileMode и
FileAccess соответственно. Таким образом, в нашем примере конструктор Fi1eStream либо создает
файл с именем MyFile.txt в текущем каталоге, либо открывает его, если файл с
таким именем уже существует. В любом случае программа сможет записывать
данные в файл. Часто встречаются и другие конструкторы класса Fi
leStream:
Допустимыми
значениями перечисляемого типа FileAccesS являются Read, Write и ReadWri te.
Основные значения перечисляемого типа Fi I eMode перечислены в табл. 9.9.
Учтите, что некоторые из них требуют особых привилегий для операций с
файлами. Таблица
9.9. Значения перечисляемого типа FileMode
Хотя файловые
потоки поддерживают произвольный доступ методом Seek, базовый класс-FileStream
ориентирован исключительно на операции с байтами, поэтому его возможности
ограничиваются простой записью байта или массива байтов методами WriteByte и
Write. Приведенный ниже фрагмент создает файл, показанный на рис.
9.2: Option Strict On Imports System.IO Module Modulel Sub
Main() Dim i As
Integer Dim theBytes(255)
As Byte For i = 0 To
255 theBytes(i) =
CByte(i) Next Dim myFileStream As
FileStream Try myFileStream = New
FileStream("C:foo", Fi1eMode.OpenOrCreate. FileAccess.Write) myFlleStream.Write(theBytes, 0. 256) Finally If Not (myFileStream Is Nothing) Then myFileStream.Close() End
Try DisplayAFile("C:foo") End Sub End
Module
Рис. 9.2.
Запись двоичных данных в файл После выполнения
этого фрагмента записанные данные можно прочитать методом Read, а также
воспользоваться методом Seek для перехода к произвольной позиции в файле.
Впрочем, как это всегда бывает при работе с неструктурированными потоками
байтов, вам придется самостоятельно преобразовать двоичные данные в более
полезный формат. В результате сейчас трудно найти более содержательный пример,
чем простой вывод записанных чисел процедурой, приведенной ниже: Sub
ReadDataBack() Dim myFileStream As
Stream.i As Integer Try myFileStream = New
FileStream("C:foo", FileMode.Open. FileAccess.Read) For i = 0 To
255 Console.Write(myFileStream.ReadByte) Next Catch e As
Exception MsgBox(e.Message) Finally If Not (myFileStream Is Nothing) Then myFileStream.Close() End Try End Sub Метод Length
базового класса Stream всегда позволяет прочитать нужное количество байтов в
цикле независимо от структуры файла. Например, следующая процедура читает файл
по одному байту. Обнаруженные исключения просто передаются вызывающей стороне;
вероятно, в реальной программе следовало бы определить новый класс
исключения: Sub DisplayAFile(ByVal theFileName As String) Dim theFile As FileStream Dim i As Long
Try theFile = New
FileStream(theFileName. Fi1eMode.Open,Fi1eAccess.Read) For i = 0 To (theFile.Length - 1) ' Вычесть 1. поскольку отсчет начинается с 0 Consolе.Write(theFiIe.ReadByte) Next Catch Throw Finally If Not (theFile Is Nothing) Then theFile.Close() End Try End Sub
Другой
распространенный вариант посимвольного чтения основан на том, что метод ReadByte
в конце потока возвращает -1. Основной цикл выглядит примерно
так: Dim i As Integer i = theFile.ReadByte Do Until i
=-1 Console.Write(i) i =
theFile.ReadByte Loop Чтение/запись
файла на уровне отдельных байтов используется не так уж часто; в основном это
необходимо при выполнении низкоуровневых операций. При операциях более высокого
уровня часто используется стандартный прием — неструктурированный файловый поток
передается конструктору потока, обладающего более широкими возможностями. Этот
принцип называется многоуровневой организацией потоков. Например,
неструктурированный файловый поток можно передать потоку, автоматически
распознающему текст. Разные способы многоуровневой организации потоков описаны в
нескольких ближайших разделах. Но прежде, чем переходить к этим разделам,
просмотрите табл. 9.10 — в ней перечислены основные методы и свойства базового
класса FileStream. В дальнейшем мы будем использовать эти методы, хотя базовый
файловый поток будет скрыт потоками более высоких уровней. Таблица
9.10. Основные члены класса RleStream
|