
Лекция 16 - Двоичные потоки.ppt
- Количество слайдов: 57
Двоичные потоки Двоичные данные хранятся в том же виде, в котором они представлены в оперативной памяти, т. е. во внутренней форме представления. Применяются для использования в программах. Выходной поток Binary. Writer поддерживает произвольный доступ, т. е. имеется возможность выполнять запись в произвольную позицию бинарного файла. Двоичный файл открывается на основе базового потока, в качестве которого чаще всего используется поток File. Stream. Входной двоичный поток содержит перегруженные методы для чтения всех простых встроенных типов данных.
Наиболее важные элементы класса Binary. Writer Элемент Описание Base. Stream Базовый поток, с которым работает объект Binary. Writer Закрыть поток Close Flush Очистить буфер Seek Установить позицию в текущем потоке Записать значение в текущий поток Write
Наиболее важные элементы класса Binary. Reader Элемент Описание Base. Stream Базовый поток, с которым работает объект Close Peek. Char Binary. Reader Закрыть поток Возвратить следующий символ без перемещения внутреннего указателя в потоке Read Считать поток байтов или символов и сохранить в массиве, передаваемом как входной параметр Read. XXX Считать из потока данные определенного типа (Read. Boolean, Read. Byte, Read. Int 32)
Пример. Формирование двоичного файла. using System; using System. IO; namespace Console. Application 1 { class Class 1 { static void Main() { try { Binary. Writer fout = new Binary. Writer(new File. Stream (@”D: C#binary”, File. Mode. Create)); double d = 0; while (d < 4) { fout. Write(d); d += 0. 33; }; fout. Seek(16, Seek. Origin. Begin); //второй элемент файла fout. Write(8888 d); fout. Close(); } catch (Exception e) { Console. Write. Line (“Error: ”+ e. Message); return; }}}}
Пример. Считывание двоичного файла. using System; using System. IO; namespace Console. Application 1 { class Class 1 { static void Main() { try { File. Stream f = new File. Stream (@”D: C#binary”, File. Mode. OPen)); Binary. Reader fin = new Binary. Reader(f); long n = f. Length/8; //количество чисел в файле double[] x = new double[n]; long i = 0; try { while (true) x[i++]= fin. Read. Double()} catch (End. Of. Stream. Exception e ){}
foreach (double d in x) Console. Write. Line( “ ”+ d); // вывод fin. Close(); f. Close; } catch (File. Noe. Found. Exception e) { Console. Write. Line(e. Message); Console. Write. Line(“Проверьте правильность имени файла!”); return; } catch (Exception e) { Console. Write. Line(“Error: “ + e. Message); return; } }}}
Результат работы программы: 0 0. 33 8888 0. 99 1. 32 1. 65 1. 98 2. 31 2. 97 3. 3 3. 63 3. 96
Консольный ввод-вывод Класс Console пространства имен System. В классе определены три стандартных потока: входной поток Console. In класса Text. Reader и выходные потоки Console. Out, Console. Error класса Text. Writer. По умолчанию входной поток связан с клавиатурой, выходные – с экраном, однако можно перенаправить их на другие устройства с помощью методов Set. In и Set. Out или средствами операционной системы. При обмене с консолью можно применять методы указанных потоков. Использование двух потоков полезно при желании разделить нормальный ввод программы и ее сообщения об ошибках.
Работа с каталогами и файлами В пространстве имен System есть четыре класса, предназначенные для работы с физическими файлами и структурой каталогов на диске: Directory, File, Directory. Info, File. Info. С их помощью можно выполнять создание, удаление, перемещение файлов и каталогов, а также получение их свойств. Классы Directory, File реализуют свои функции через статические методы. Directory. Info, File. Info обладают схожими возможностями, но они реализуются путем создания объектов соответствующих классов. Классы Directory. Info, File. Info происходят от абстрактного класса File. System. Info, который снабжает их базовыми свойствами.
Свойства класса File. System. Info Attributes Получить или установить атрибуты для данного объекта файловой системы. Используются значения перечисления File. Attributes Creation. Time Получить или установить время создания объекта файловой системы Exists Определить, существует файловой системы Extension Получить расширение файла Full. Name Возвратить имя файла или каталога с указанием полного пути Last. Access. Time Получить или установить время последнего обращения к объекту файловой системы Last. Write. Time Получить или установить время последнего внесения изменений объекта файловой системы Name Возвратить имя файла. Доступно только для чтения. Для каталогов возвращает имя последнего каталога в иерархии ли данный объект
Элементы класса Directory. Info Create, Create. Sub. Directory Delete Создать каталог или подкаталог по указанному пути в файловой системе Get. Directories Возвратить массив строк, представляющих все подкаталоги Get. Files Получить все файлы в текущем каталоге в виде массива объектов класса File. Info Move. To Переместить каталог и все его содержимое на новый адрес в файловой системе Parent Возвратить родительский каталог Удалить каталог содержимым со всем его
Пример. Использование класса Directory. Info. using System; using System. IO; namespace Console. Application 1 { class Class 1 {static void Dir. Info (di); { Console. Write. Line(“===Directory. Info===”); Console. Write. Line(“Full. Name: ”+di. Full. Name); Console. Write. Line(“Name: ”+di. Name); Console. Write. Line(“Parent: ”+di. Parent); Console. Write. Line(“Creation: ”+di. Creation. Time); Console. Write. Line(“Attributes: ”+di. Attributes); Console. Write. Line(“Root: ”+di. Root); Console. Write. Line(“====”); }
static void Main () { Directory. Info d 1 = new Directory. Info (@”c: My. Dir”); Directory. Info d 2 = new Directory. Info (@”c: My. Dirtemp”); try { d 1. Create(); d 2. Create(); Dir. Info(d 1); Dir. Info(d 2); Console. Write. Line(“Попытка удалить {0}”, d 1. Name); d 1. Delete(); } catch (Exception) {Console. Write. Line(“Попытка не удалась. ”); } } }}
Результат работы программы ===Directory. Info=== Full. Name: c: My. Dir Name: My. Dir Parent: Creation: 04. 03. 2009 21: 05 Attributes: Directory Root: c: ==== ===Directory. Info=== Full. Name: c: My. Dirtemp Name: temp Parent: My. Dir Creation: 04. 03. 2009 21: 05 Attributes: Directory Root: c: ==== Попытка удалить My. Dir Попытка не удалась.
Каталог не пуст, поэтому попытка его удаления не выполнена. Если использовать перегруженный вариант метода Delete с одним параметром, задающим режим удаления, можно удалить и непустой каталог: d 1. Delete(true);
Некоторые значения перечисления File. Attributes Archive Используется приложениями при выполнении резервного копирования, а в некоторых случаях – при удалении старых файлов Compressed Файл является сжатым Directory Объект файловой системы является каталогом Encrypted Файл является зашифрованным Hidden Файл является скрытым Normal Файл находится в обычном состоянии, и для него установлены любые другие атрибуты. Не может использоваться с другими атрибутами Offline Файл, расположенный на сервере, кэширован, в хранилище на клиентском компьютере. Возможно, что данные уже устарели Read. Only Файл доступен только для чтения System Файл является системным
Пример. Копирование файлов. using System; using System. IO; namespace Console. Application 1 { class Class 1 { static void Main() { try { string Dest. Name = @”d: temp”; Directory. Info dest = new Directory. Info (Dest. Name); dest. Create(); //создание целевого каталога Directory. Info dir = new Directory. Info(@ “d: foto”); if (! Dir. Exists) //проверка существования каталога {Console. Write. Line(“Каталог ”+dir. Name+” не существует”); return; } File. Info[] files = dir. Get. Files( “*. jpg”); //список файлов for each (File. Info f in files) f. Copy. To(dest + f. Name); //копирование файлов Console. Write. Line(“Скопировано”+ files. Length +“ –файлов”); } catch (Exception e) { Console. Write. Line(“Error: ”+ e. Message); } } }}}
Использование классов File и Directory аналогично, за исключением того, что их методы являются статическими, и, следовательно, не требуют создания объектов.
Сохранение объектов (сериализация) В C# есть возможность сохранять на внешних носителях не только данных примитивных типов, но и объекты. Сохранение объектов называется сериализацией, а восстановление сохраненных объектов – десериализацией. При сериализации объект преобразуется в линейную последовательность байтов. Это сложный процесс, поскольку объект может включать множество унаследованных полей и ссылки на вложенные объекты, которые, в свою очередь, тоже могут состоять из объектов сложной структуры.
Сериализация выполняется автоматически, достаточно просто пометить класс как сериализуемый помощью атрибута [Serializable]. Атрибуты – дополнительные сведения о классе, которые сохраняются в его метаданных. Те поля, которые сохранять не требуется, помечаются атрибутом [Non. Serialized], например: [Serializable] class Demo { public int a = 1; [Non. Serialized] public double y; public Monster X, Y; }
Объекты можно сохранять в одном из двух форматах: двоичном или SOAP (в виде XML -файла). В первом случае следует подключить к программе имен System. Runtime. Serialization. Formatters. Binary во втором – System. Runtime. Serialization. Formatters. Soap
Для сохранения объектов в двоичном формате этого используется класс Binary. Formatter, в котором определены два метода: Serialize(поток, объект); Deserialize(поток); Метод Serialize сохраняет заданный объект в заданном потоке, метод Deserialize восстанавливает объект из заданного потока.
• В пространстве имен библиотеки FCL: System. Runtime. Serialization. Formatters. Soap находится класс Soap. Formatter. Он является наследником интерфейсов IFormatter и IRemoting. Formatter и реализует их методы Serialize и Deserialize, позволяющие выполнять глубокую сериализацию и десериализацию при сохранении данных в формате xml. Помимо методов класса Soap. Formatter, xmlсериализацию можно выполнять средствами другого класса – Xml. Serializer.
Пример. Сериализация объекта. using System; using System. IO; using System. Runtime. Serialization. Formatters. Binary; namespace Console. Application 1 { [Serializable] abstract class Spirit {public abstract void Passport(); } [Serializable] class Monster: Spirit { public Monster (int health, int ammo, string name) { this. health = health; this. ammo = ammo; this. name = name; } override public void Passport () { Console. Write. Line(“Monster {0} t health = {1} ammo = {2}”, name, health, ammo); } string name; int health, ammo; }
[Serializable] class Demo { public int a = 1; [Non. Serializable] public double b; public monster X, Y; } class Class 1 { static void Main() { Demo d = new Demo(); d. X = new Monster (100, 80, “Вася”); d. Y = new Monster (120, 50, “Петя”); d. a = 2; d. b = 2; d. X. Passport(); d. Y. Passport(); Console. Write. Line(d. a); Console. Write. Line(d. b); File. Stream f = new File. Stream(“Demo. bin”, File. Mode. Create); Binary. Formatter bf = new Binary. Formatter(); bf. Serialize(f, d); //сохранение объекта d в потоке f f. Close(); }}}
Monster Вася health = 100 Monster Петя health = 120 2 2 ammo = 80 ammo = 50 Базовые классы сохраняемых объектов также должны быть помечены как сохраняемые.
Для сохранения объекта в двоичном формате необходимо: 1. Подключить к программе пространство имен System. Runtime. Serialization. Formatters. Binary 2. Пометить сохраняемый класс и связанные с ним объекты атрибутом [Serializable] 3. Создать поток и связать его с файлом на диске или с областью оперативной памяти. 4. Создать объект класса Binary. Formatter. 5. Сохранить объект в потоке. 6. Закрыть файл.
Пример. Десериализация объекта. using System; using System. IO; using System. Runtime. Serialization. Formatters. Binary; namespace Console. Application 1 { class Class 1 { static void Main() { File. Stream f = new File. Stream(“Demo. bin”, File. Mode. Open); Binary. Formatter bf = new Binary. Formatter(); Demo d = (Demo) bf. Deserialize(f); //восстановление объекта d. X. Passport(); d. Y. Passport(); Console. Write. Line(d. a); Console. Write. Line(d. b); f. Close(); }}}
Monster Вася health = 100 Monster Петя health = 120 2 0 ammo = 80 ammo = 50 При сериализации сохраняется все дерево объектов.
Еще пример Промоделируем сказку Пушкина "О рыбаке и рыбке". Жадная старуха богатела, но после очередного желания оказалась у разбитого корыта, вернувшись в начальное состояние. Сериализация позволит нам запомнить начальное состояние, меняющееся по мере выполнения рыбкой первых пожеланий рыбака и его старухи. Десериализация вернет все в начальное состояние. Класс, задающий героев сказки: [Serializable] public class Personage { public Personage(string name, int age) { this. name = name; this. age = age; } //поля класса static int wishes; public string name, status, wealth; int age; public Personage couple; //методы класса}
Герои сказки – объекты этого класса обладают свойствами, задающими имя, возраст, статус, имущество и супруга. Имя и возраст задаются в конструкторе класса, а остальные свойства задаются в следующем методе: public void marry (Personage couple) { this. couple = couple; couple = this; this. status ="крестьянин"; this. wealth ="рыбацкая сеть"; this. couple. status = "крестьянка"; this. couple. wealth = "корыто"; Save. State(); }
Предусловие метода предполагает, что метод вызывается один раз главным героем (рыбаком). В методе устанавливаются взаимные ссылки между героями сказки, их начальное состояние. Завершается метод сохранением состояния объектов, выполняемого при вызове метода Save. State: void Save. State() { Binary. Formatter bf = new Binary. Formatter(); File. Stream fs = new File. Stream ("State. bin", File. Mode. Create, File. Access. Write); bf. Serialize(fs, this); fs. Close(); }
Метод, описывающий жизнь героев сказки: public Personage Ask. Gold. Fish() { Personage fisher = this; if (fisher. name == "рыбак") { wishes++; switch (wishes) { case 1: Change. State. One(); break; case 2: Change. State. Two(); break; case 3: Change. State. Three(); break; default: Back. State(ref fisher); break; return(fisher); }//Ask. Gold. Fish } }
Метод реализует анализ желаний героини сказки. Первые три желания исполняются, и состояние героев меняется: void Change. State. One() { this. status = "муж дворянки"; this. couple. status = "дворянка"; this. couple. wealth = "имение"; } void Change. State. Two() { this. status = "муж боярыни"; this. couple. status = "боярыня"; this. couple. wealth = "много поместий"; } void Change. State. Three() { this. status = "муж государыни"; this. couple. status = "государыня"; this. couple. wealth = "страна"; }
Начиная с четвертого желания, все возвращается в начальное состояние – выполняется десериализация графа объектов: void Back. State(ref Personage fisher) { Binary. Formatter bf = new Binary. Formatter(); File. Stream fs = new File. Stream ("State. bin", File. Mode. Open, File. Access. Read); fisher = (Personage)bf. Deserialize(fs); fs. Close(); }
У метода есть аргумент, передаваемый по ссылке. Этот аргумент получает значение – ссылается на объект, создаваемый методом Deserialize. Без аргумента метода не обойтись, поскольку возвращаемый методом объект нельзя присвоить текущему объекту this. Важно также отметить, что метод Deserialize восстанавливает весь граф объектов, возвращая в качестве результата корень графа. В классе определен еще один метод, сообщающий о текущем состоянии объектов: public void About() { Console. Write. Line("имя = {0}, возраст = {1}, "+ "статус = {2}, состояние ={3}", name, age, status, wealth); Console. Write. Line("имя = {0}, возраст = {1}, " + "статус = {2}, состояние ={3}", this. couple. name, this. couple. age, this. couple. status, this. couple. wealth); }
Для завершения сказки нам нужно в клиентском классе создать ее героев: public void Test. Gold. Fish() { Personage fisher = new Personage("рыбак", 70); Personage wife = new Personage("старуха", 70); fisher. marry(wife); Console. Write. Line("До золотой рыбки"); fisher. About(); fisher = fisher. Ask. Gold. Fish(); Console. Write. Line("Первое желание"); fisher. About(); fisher = fisher. Ask. Gold. Fish(); Console. Write. Line("Второе желание"); fisher. About(); fisher = fisher. Ask. Gold. Fish(); Console. Write. Line("Третье желание"); fisher. About(); fisher = fisher. Ask. Gold. Fish(); Console. Write. Line("Еще хочу"); fisher. About(); fisher = fisher. Ask. Gold. Fish(); Console. Write. Line("Хочу, но уже поздно"); fisher. About(); }
Если перейти к сохранению данных в xml-формате, то изменится немногое. Нужно лишь заменить объявление форматера: void Save. State. XML() { Soap. Formatter sf = new Soap. Formatter(); File. Stream fs = new File. Stream ("State. xml", File. Mode. Create, File. Access. Write); sf. Serialize(fs, this); fs. Close(); } void Back. State. XML(ref Personage fisher) { Soap. Formatter sf = new Soap. Formatter(); File. Stream fs = new File. Stream("State. xml", File. Mode. Open, File. Access. Read); fisher = (Personage)sf. Deserialize(fs); fs. Close(); }
Клиент, работающий с объектами класса, этих изменений и не почувствует. Результаты вычислений останутся теми же, что и в предыдущем случае. Файл, сохраняющий данные, теперь естьобычный xml-документ, который мог быть создан в любом из приложений. Вот как выглядит этот документ, открытый в браузере Internet Explorer.
Интерфейс ISerializable При необходимости можно самому управлять процессом сериализации. В этом случае класс должен быть наследником интерфейса ISerializable. Класс, наследующий этот интерфейс, должен реализовать единственный метод этого интерфейса Get. Object. Data и добавить защищенный конструктор. Схема сериализации и десериализации остается и в этом случае той же самой. Можно использовать как бинарный форматер, так и soap-форматер. Но теперь метод Serialize использует не стандартную реализацию, а вызывает метод Get. Object. Data, управляющий записью данных. Метод Deserialize, в свою очередь, вызывает защищенный конструктор, создающий объект и заполняющий его поля сохраненными значениями.
Конечно, возможность управлять сохранением и восстановлением данных дает большую гибкость и позволяет, в конечном счете, уменьшить размер файла, хранящего данные, что может быть крайне важно, особенно если речь идет об обмене данными с удаленным приложением. Если речь идет о поверхностной сериализации, то атрибут Non. Serialized, которым можно помечать поля, не требующие сериализации, как правило, достаточен для управления эффективным сохранением данных. Так что управлять имеет смысл только глубокой сериализацией, когда сохраняется и восстанавливается граф объектов.
Рассмотрим, как устроен метод Get. Object. Data, управляющий сохранением данных. У этого метода два аргумента: • Get. Object. Data(Serialized. Info info, Streaming. Context context) Поскольку самому вызывать этот метод не приходится – он вызывается автоматически методом Serialize, то можно не особенно задумываться о том, как создавать аргументы метода. Более важно понимать, как их следует использовать. Чаще всего используется только аргумент info и его метод Add. Value (key, field). Данные сохраняются вместе с ключом, используемым позже при чтении данных. Аргумент key, который может быть произвольной строкой, задает ключ, а аргумент field – поле объекта.
Например, для сохранения полей name и age можно задать следующие операторы: info. Add. Value("name", name); info. Add. Value("age", age); Поскольку имена полей уникальны, то их разумно использовать в качестве ключей. Если поле son класса Father является объектом класса Child и этот класс сериализуем, то для сохранения объекта son следует вызвать метод: son. Get. Object. Data(info, context) Если не возникает циклов, причиной которых являются взаимные ссылки, то особых сложностей с сериализацией и десериализацией не возникает. Взаимные ссылки осложняют картину и требуют индивидуального подхода к решению. На последующем примере мы покажем, как можно справиться с этой проблемой в конкретном случае
Специальный конструктор класса. Может быть объявлен с атрибутом доступа private, но лучше, как и во многих других случаях, применять атрибут protected, что позволит использовать этот конструктор потомками класса, осуществляющими собственную сериализацию. У конструктора те же аргументы, что и у метода Get. Object. Data. В основном используется аргумент info и его метод Get. Value(key, type), который выполняет операцию, обратную к операции метода Add. Value. По ключу key находится хранимое значение, а аргумент type позволяет привести его к нужному типу. У метода Get. Value имеется множество типизированных версий, позволяющих не задавать тип. Так что восстановление полей name и age можно выполнить следующими операторами: name = info. Get. String("name"); age = info. Get. Int 32("age");
Восстановление поля son, являющегося ссылочным типом, выполняется вызовом его специального конструктора: son = new Child(info, context); Вернемся к нашему примеру со стариком, старухой и золотой рыбкой. Заменим стандартную сериализацию собственной. Для этого, оставив атрибут сериализации у класса Personage, сделаем класс наследником интерфейса ISerializable: [Serializable] public class Personage : ISerializable {. . . }
Добавим в наш класс специальный метод, вызываемый при сериализации - метод сохранения данных: //Специальный метод сериализации public void Get. Object. Data(Serialization. Info info, Streaming. Context context) { info. Add. Value("name", name); info. Add. Value("age", age); info. Add. Value("status", status); info. Add. Value("wealth", wealth); info. Add. Value("couplename", couple. name); info. Add. Value("coupleage", couple. age); info. Add. Value("couplestatus", couple. status); info. Add. Value("couplewealth", couple. wealth); }
В трех первых строках сохраняются значимые поля объекта и тут все ясно. Но вот запомнить поле, хранящее объект couple класса Personage, напрямую не удается. Попытка рекурсивного вызова couple. Get. Object. Data(info, context); привела бы к зацикливанию, если бы раньше изза повторяющегося ключа не возникала исключительная ситуация в момент записи поля name объекта couple. Поэтому приходится явно сохранять поля этого объекта уже с другими ключами. Понятно, что с ростом сложности структуры графа объектов задача существенно осложняется.
• конструктор восстановления состояния: • //Специальный конструктор сериализацииprotected Personage(Serialization. Info info, Streaming. Context context) { name = info. Get. String("name"); age = info. Get. Int 32("age"); status = info. Get. String("status"); wealth = info. Get. String("wealth"); couple = new Personage(info. Get. String("couplename"), info. Get. Int 32("coupleage")); couple. status = info. Get. String("couplestatus"); couple. wealth = info. Get. String("couplewealth"); this. couple = couple; couple = this; }
Поле couple: вначале создается новый объект обычным конструктором, аргументы которого читаются из сохраняемой памяти. Затем восстанавливаются значения других полей этого объекта, а затем уже происходит взаимное связывание двух объектов. Кроме введения конструктора класса и метода Get. Object. Data, никаких других изменений в проекте не понадобилось - ни в методах класса, ни на стороне клиента. Внешне проект работал совершенно идентично ситуации, когда не вводилось наследование интерфейса сериализации. Но с внутренних позиций изменения произошли: методы форматеров Serialize и Deserialize в процессе своей работы теперь вызывали созданный нами метод и конструктор класса. Небольшие изменения произошли и в файлах, хранящих данные.
Мораль: должны быть веские основания для отказа от стандартно реализованной сериализации. Такими основаниями могут служить необходимость в уменьшении объема файла, хранящего данные, и в сокращении времени передачи данных. Преимуществами XML-документа являются его читабельность и хорошо развитые средства разбора, но зато бинарное представление выигрывает в объеме и скорости передачи тех же данных.
Summary Большинство программ тем или иным образом работают с внешними устройствами, в качестве которых могут выступать, например, консоль, файл на диске или сетевое соединение. Взаимодействие с внешними устройствами организуется с помощью потоков, которые поддерживаются множеством классов библиотеки. Net. Поток передается как последовательность байтов и не зависит от конкретного устройства, с которым производится обмен. Классы библиотеки позволяют работать с потоками в различных режимах и на различных уровнях: на уровне двоичного представления данных, байтов и текста. Двоичные и байтовые потоки хранят во внутреннем представлении, текстовые – в кодировке Unicode.
Поток можно открыть в синхронном и асинхронном режиме для чтения, записи или добавления. Доступ к файлам может быть последовательными или произвольным. Текстовые файлы позволяют выполнять только последовательный доступ, в двоичных и байтовых потоках можно использовать оба метода. Прямой доступ в сочетании с отсутствием преобразований обеспечивает высокую скорость обмена. Методы форматированного ввода для значений арифметических типов в С# не поддерживаются. Для преобразования из символьного в числовое используются методы класса Convert или метод Parse. Форматированный вывод выполняется с помощью перегруженного метода To. String, результат выполнения которого передается в методы текстовых файлов.
Рекомендуется всегда проверять успешность открытия существующего файла, перехватывать исключения, возникающие при преобразовании значений арифметических типов, и явным образом закрывать файл, в который выполнялась запись. Длительные операции с файлами эффективнее выполнять в асинхронном режиме. Для сохранения объектов (сериализации) используется атрибут [Serializable]. Объекты можно сохранять в одном из двух форматов: двоичном или SOAP(в виде XML-файла).