Сериализация коллекций объектов. ТВЕРСКОЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ Кафедра ЭВМ Программирование на языке C# в среде Microsoft Visual Studio. Сериализация. Методические указания к лабораторным работам по курсу "Объектно-ориентированное программирование" Лабораторная работа № 4 Тверь 2012 Цель лабораторной работы заключается в изучении основных принципов сохранения состояния объектов путем их сериализации и приобретении практических навыков по их реализации и применению при разработке программных приложений в интегрированной среде Visual Studio. Основными задачами, решаемыми в процессе выполнения лабораторной работы, являются: · Изучение особенностей основных принципов и механизмов, используемых для сериализации и десериализации программных объектов. · Изучение особенностей сохранения и восстановления состояния программных объектов различной сложности путем их сериализации и десериализации. Методическое указание обсуждено на заседании кафедры ЭВМ (протокол №___от 2002_ года) и рекомендовано к печати. Составитель: Веселов А.А. Содержание | | | | | Стр. | | | | | Теоретическая часть. | | | 1.1 | | | Сериализация | | | 1.2 | | | Десериализация | | | 1.3 | | | Сериализация коллекций объектов | | | | 1.3.1 | | Описание объекта | | | | 1.3.2 | | Роль объектных графов | | | | 1.3.3 | | Настройка объектов для сериализации | | | | 1.3.4 | | Главная форма приложения | | | | 1.3.5 | | Настройка фильтра диалогового окна для открытия файлов | | | Указания к выполнению лабораторной работы | | | 2.1 | Задание на лабораторную работу | | | 2.2 | Содержание отчета по лабораторной работе | | | Литература | | Теоретическая часть. Сериализация. Под сериализацией понимают процесс сохранения состояния объекта в дисковом файле, памяти или в каком-либо другом месте. В процессе сериализации все данные экземпляра сохраняются на некотором носителе, а в процессе десериализации объект снова наполняется таким образом, что его становится невозможно отличить от первоначального экземпляра. Сохранив состояние объекта Все, кто был вынужден постоянно думать о том, каким образом сохранить или читать данные некоего экземпляра, поймут, от какого объема вводимой информации может избавить этот атрибут. Предположим, что у вас имеется некоторый объект, который необходимо сохранить в файле и который описывается следующим образом: public class Person { public Person() {} public int Age; public int WeightPound; } В С# есть возможность сериализовать члены данного экземпляра без написания какого-либо кода. Все, что вас требуется,— это включить в класс атрибут [Serializable], а все остальное сделает за вас система выполнения программ .NET. Когда в названную систему поступает запрос на сериализацию какого-нибудь объекта, то она проверяет, реализован ли в классе данного объекта интерфейс ISerializable, И если нет, то имеется ли в этом классе атрибут Serializable. Если в классе обнаруживается атрибут Serializable, то .NET использует отражение для того, чтобы получить все данные конкретного экземпляра — независимо от того, описаны ли они как общие, защищенные или частные, и сохранить их как представление этого объекта. Десериализация представляет собой процесс, обратный описанному, — данные считываются с носителя информации и присваиваются переменным экземплярам данного класса. Ниже приводится пример класса, помеченного атрибутом Serializable: [Serializable] public class Person { public Person() {} public int Age; public int WeightInPound; } Для того, чтобы сохранить экземпляр класса Person, используется объект Formatter, который преобразовывает данные, хранящиеся в классе, в последовательность байтов. Система поставляется с двумя типами таких объектов, используемых по умолчании: BinaryFormatter и SoapFormatter. Программа, приведенная ниже, демонстрирует, каким образом можно использовать BinaryFormatter для сохранения объекта класса Person. public static void Serialize() { // Создание объекта класса Person Person me = Person(); // Ввод данных, подлежащих серализации me.Age = 24; me.WeightPound = 200; // Использование BinaryFormatter для записи этого объекта в поток BinaryFormatter bf = new BinaryFormatter(); // Создание на диске файла, предназначенного для хранения данного объекта FileStream s = File.Open("me.dat", FileMode.Create, FileEccess.Write); // Сериализация объекта bf.Serialize(s, me); // Закрытие потока s.Close(); } В этой программе вначале создается объект класса Person и задаются значения переменным Age и WeightPound, характеризующим его конкретное состояние. После этого создается экземпляр класса BinaryFormatter. Этот класс предназначен для сериализации и десериализации объектов или даже целой совокупности связанных объектов (объектный граф) в потоке в бинарном формате. Но, прежде чем сохранить непосредственно данные, необходимо создать поток. В данном примере поток является экземпляром класса FileStream, который создает новый файл и открывает его для чтения. Теперь есть все необходимое для сериализации. Для этого у объекта форматера типа BinaryFormatter вызывается метод Serialize(s, me). Этому методу передаются два аргумента: поток, в котором будет происходить сохранение, и объект, который необходимо сохранить в данном потоке. Для простых объектов этого вполне достаточно, а дальше объект сериализации (форматер) сделает все необходимое по сохранению значений всей совокупности переменных у объекта типа Person в указанный поток в бинарном формате. Когда сохранение будет закончено, то поток закрывается. Таким образом, четыре строчки программного кода позволили сохранить состояния объекта. Причем три из них относились к инициализации потока и форматировщика и закрытию потока. В некоторых случаях может потребоваться указать одно или несколько полей, не подлежащих сериализации. Этого можно добиться следующим образом: [Serializable] public class Person { public Person() {} public int Age; [NonSerialized] public int WeightInPound; } В процессе сериализации такого класса будет сохраняться только член класса Age, а член WeightInPound сохраняться не будет и, следовательно, не будет извлекаться при лесериализации. Десериализация. Действия при десериализация, в основном, обратны действиям, соответствующим сериализации. Следующий пример открывает поток в виде файла Me.dat, создает объект типа BinaryFormatter для считывания объекта и вызывает метод Deserialize(), позволяющий восстаовить состояние этого объекта. После этого считанные данные приводятся к типу Person, и на консоль выводятся возраст и вес: public static void DeSerialize() { // На этот раз сначала открывается файл FileStream s = File.Open ("Me.dat" , FileMode.Open, FileAccess.Read); // BinaryFormatted используется для чтения объекта (объектов) из потока BinaryFormatter bf = new BinaryFormatter() ; // Десериализация объекта object о = bf.Deserialize(s) ; //Убеждаемся в том, что объект нужного нам типа Person р = о as Person; if(р!=null) Console.WriteLine("DeSerialized Person aged: {0} weight: {1}", p.Age,p.WeightInPounds); // Закрытие потока s.Close(); } В самом начале создается экземпляр класса BinaryFormatter, который используется для десериализации данных из файла. Затем создается объект потока типа FileStream, который будет представлять поток файла. Этот поток создается с параметрами FileMode.Open, чтобы открыть файл, и FileAccess.Read для того, чтобы можно было считывать данные из открытого потока. Самое интересное в программном коде заключается в вызове метода Deserialize(s) у форматера типа BinaryFormatter. Этот метод получает только лишь один аргумент в виде потока, из которого следует считывать данные. Метод читает данные в поисках полей данных (или свойств) сохраненного объекта и возвращает его в виде результата своей работы. Причем метод сам создает объект и инициализирует его сохраненными данными. Поэтому нам и не нужно инициализировать объект типа Person. В результате мы получаем объект типа Person и нам остается только привести типы. Но как же метод узнает класс создаваемого им объекта, который он десериализировал из потока? Дело в том, что информация о классе также сохраняется в потоке. Объекты можно сохранять не только в файлах, но и в потоке памяти MemoryStream. Например, перед выполнением каких-либо расчетов можно сохранить состояние объекта в потоке памяти и восстановить это состояние после выполнения расчетов. В такие моменты потоки в памяти предоставляют всю мощь и скорость доступа. Обычно, с помощью атрибута NonSerialized (не подлежащие сериализации) помечаются данные, которые получаются в результате вычислений, поскольку они могут быть вычислены по мере возникновения такой необходимости. В качестве примера можно привести класс, в котором вычисляются простые числа. Их вполне можно сохранять для увеличения скорости ответа при обращении к данному классу, однако сериализация и десериализация списка простых чисел не является необходимой, поскольку они всегда могут быть вычислены в момент обращения к классу. В других случаях некоторый член может иметь отношение только к данному экземпляру документа. Например, для объекта, который представляет документ, обрабатываемый редактором, потребуется сериализовать содержимое самого документа, но не текущую позицию, поскольку при очередной загрузке документа в качестве такой позиции просто используется начало документа. Сериализация коллекций объектов. Описание объекта Теперь, когда известно, как сохранять единственный объект в потоке, давайте посмотрим, каким образом можно сохранить множество объектов. Как вы, возможно, заметили, метод Serialize() интерфейса IFormatter не предусматривает способа указать произвольное количество объектов в качестве ввода (допускается только один объект System.Object). Вдобавок возвращаемое значение Deserialize() также представляет собой одиночный объект System.Object (то же базовое ограничение касается и XmlSerializer): public interface IFormatter { object Deserialize(Stream serializationStream); void Serialize(Stream serializationStream, object graph); } Однако, если существует сериализуемый объект (помеченный атрибутом [Serializable]), который содержит в себе другие сериализуемые объекты (тоже помеченные атрибутом [Serializable]), то с помощью единственного вызова метода Serialize() будет сохраняться весь набор объектов. К счастью, большинство типов из пространств имен System.Collections и System.Collections.Generic уже помечены атрибутом [Serializable]. Таким образом, чтобы сохранить множество объектов, просто добавьте это множество в контейнер (такой как ArrayList или List<T>) и сериализуйте данный объект в выбранный поток. Предположим, что у нас есть класс JamesBondCar, дополненный конструктором, принимающим два аргумента, для установки нескольких фрагментов данных состояния (обратите внимание, что должен быть также добавлен конструктор по умолчанию, как того требует XmlSerializer): [Serializable] public class Radio //Класс радиоприемника { public Radio(){} public void On(bool state) { if (state == true) MessageBox.Show("Радио включено!"); else MessageBox.Show("Радио выключено!"); } } [Serializable] public class Car // Класс обычного автомобиля { protected string petName; protected int maxSpeed; protected Radio theRadio = new Radio(); public Car() { petName = "Noname"; maxSpeed = 0; } public Car(string PetName, int maxSpeed) { this.petName = PetName; this.maxSpeed = maxSpeed; } // Пусть значения всех переменных будут автоматически установлены по умолчанию public string PetName { get { return petName; } set { petName = value; } } public int MaxSpeed { get { return maxSpeed; } set { maxSpeed = value; } } public void TurnOnRadio(bool state) { theRadio.On(state); } } [Serializable, //XmlRoot (Namespace = "http://www.MyCompany.com")] public class JamesBondCar : Car // Класс специального автомобиля { bool canFly; bool canSubmerge; // XmlSerializer требует конструктора по умолчанию! public JamesBondCar(){} public JamesBondCar(bool skyWorthy, bool seaworthy) { canFly = skyWorthy; canSubmerge = seaworthy; } public bool CanFly { set { canFly = value; } get { return canFly; } } public bool CanSubmerge { set { canSubmerge = value; } get { return canSubmerge; } } public string GetFly() { if (canFly) return "Может летать!"; else return "Не может летать!"; } public string GetSubmerge() { if (canSubmerge) return "Может плавать под водой!"; else return "Не может плавать - тонет!"; } } [Serializable] public class CarProvider { private List<JamesBondCar> theJBCars = new List<JamesBondCar>(); // Добавить несколько автомобилей в список. public CarProvider(){ } public CarProvider(bool pr) { if (!pr) return; theJBCars.Add(new JamesBondCar("QMobile", 140, true, true)); theJBCars.Add(new JamesBondCar("Flyer", 150, true, false)); theJBCars.Add(new JamesBondCar("Swimmer", 160, false, true)); theJBCars.Add(new JamesBondCar("BasicJBC", 170, false, false)); } public List<JamesBondCar> Cars { get { return theJBCars; } set { Cars = value; } } // Получить все JamesBondCar. public List<JamesBondCar> GetAllAutos(){ return theJBCars; } // Получить один JamesBondCar. public JamesBondCar GetJBCByIndex(int i){ return (JamesBondCar)theJBCars[i]; } public void AddNewJBCar(string petName, int maxSpeed, bool canFly, bool canSubmerge) { theJBCars.Add(new JamesBondCar(petName, maxSpeed, canFly, canSubmerge)); } public int GetCarNum() { return theJBCars.Count; } public void ClearCarList() { theJBCars.Clear(); } } Роль объектных графов. При сериализации объекта CLR учитывает состояния всех связанных объектов. Множество связанных объектов представляется объектным графом. Объектные графы обеспечивают простой способ учета взаимных связей в множестве объектов, и совсем не обязательно, чтобы эти связи в точности проецировались в классические связи объектно-ориентированного программирования (такие, как отношения наследования или вложенности), хотя они достаточно хорошо моделируют и такую парадигму. В объектном графе каждому объекту назначается уникальное числовое значение. Однако, следует иметь в виду, что числовые значения, приписываемые членам в объектном графе, являются произвольными и не имеют никакого смыслового значения вне самого графа. После назначения всем объектам числового значения, объектный граф может начать запись множества зависимостей между его объектами. В качестве простого примера предположим, что создан набор классов, моделирующих автомобили. Существует базовый класс по имени Саr, который "имеет" класс Radio. Другой класс по имени JamesBondCar расширяет базовый тип Саг. На рис.1 показан возможный граф объектов, который моделирует эти отношения.  Рис. 1. Простой граф объектов. При чтении графов объектов для описания соединяющих стрелок можно использовать выражение "зависит от" или "ссылается на". Таким образом, на рис.1 видно, что класс Саr ссылается на класс Radio (учитывая отношение "имеет"), а класс JamesBondCar ссылается на класс Саr (учитывая отношение "имеет") и на класс Radio (поскольку наследует эту защищенную переменную-член). Конечно, среда CLR не рисует картинок в своей памяти для представления графа взаимосвязанных объектов. Вместо этого взаимосвязи, указанные в диаграмме, представляются математической формулой, которая выглядит примерно так: [Саr 3, ref 2], [Radio 2], [JamesBondCar 1, ref 3, ref 2] Проанализировав эту формулу, можно увидеть, что объект 3 (Саr) имеет зависимость от объекта 2 (Radio). Объект 2 (Radio) является "индивидуалистом", которому не нужен никто. И, наконец, объект 1 (JamesBondCar) имеет зависимость как от объекта 3, так и от объекта 2. В любом случае, при сериализации или десериализации (реконструкция, восстановление) экземпляра типа JamesBondCar, объектный граф гарантирует, что типы Radio и Саr также будут участвовать в процессе. Изящество процесса сериализации состоит в том, что граф, представляющий отношения между объектами, создается автоматически в фоновом режиме. Если необходимо вмешаться в конструирования графа объектов, то это можно сделать с помощью настройки процесса сериализации через ее атрибуты и интерфейсы. |