Сериализация на данни Автор
Радослав Иванов
Необходими знания -
Базови познания за .NET Framework, CLR (Common Language Runtime) и общата система от типове в .NET (Common Type System)
-
Познания за езика C#
-
Познания за работа с потоци от данни
-
Познания по отражение на типовете (reflection)
-
Познания за атрибутите в .NET Framework
-
Познания за работа с XML в .NET Framework
Съдържание -
Какво e сериализация? Кога и защо се използва?
-
Форматери (Formatters)
-
Процесът на сериализация
-
Сериализация и десериализация – пример
-
Пример за бинарна сериализация
-
Пример за сериализация по мрежата
-
Пример за дълбоко копиране на обекти
-
IDeserializationCallback
-
Контролиране на сериализацията. ISerializable
-
XML сериализация
-
Контролиране на изходния XML
В тази тема ...
В настоящата тема ще разгледаме сериализацията на данни в .NET Framework. Ще обясним какво е сериализация, за какво се използва и как да контролираме процеса на сериализация. Ще се запознаем с видовете форматери (formatters). Ще обясним какво е XML сериализация, как работи тя и как можем да контролираме изходния XML при нейното използване.
Сериализация
В съвременното програмиране често се налага да се съхрани състоянието на даден обект от паметта и да се възстанови след известно време. Това позволява обектите временно да се съхраняват на твърдия диск и да се използват след време, както и да се пренасят по мрежата и да се възстановяват на отдалечена машина.
Проблемите при съхранението и възстановяването на обекти са много и за справянето с тях има различни подходи. За да се намалят усилията на разработчиците в .NET Framework е изградена технология за автоматизация на този процес, наречена сериализация. Нека се запознаем по-подробно с нея.
Какво е сериализация (serialization)?
Сериализацията е процес, който преобразува обект или свързан граф от обекти до поток от байтове, като запазва състоянието на неговите полета и свойства. Потокът може да бъде двоичен (binary) или текстов (XML).
Какво е десериализация (deserialization)?
Обратният процес на сериализацията е десериализацията. Десериализацията е процеса на преобразуване на поток от байтове обратно до обект. Десериализираният (възстановеният) обект запазва състоянието на оригиналния обект (стойностите в полетата и свойствата си).
Кога се използва сериализация?
Ще разгледаме някои от най-честите приложения на сериализацията и десериализацията.
Запазване на състоянието на обект
Сериализацията се използва за съхранение на информация и запазване на състоянието на обекти. Използвайки сериализация, дадена програма може да съхрани състоянието си във файл, база данни или друг носител и след време да го възстанови обратно.
Предаване на обект през комуникационна мрежа
Сериализацията може да се използва за предаване на обекти през мрежа. За целта обектът се сериализира и се транспортира през мрежата, след което се десериализира, за да се пресъздаде абсолютно същия обект, който е бил изпратен. Примерно приложение на този метод е за предаване на данни между две програми.
Приложение вътрешно в .NET Framework
Технологиите от .NET Framework използват вътрешно сериализация за някои задачи, например:
-
за запазване на състоянието на сесията (т. нар. "session state") в ASP.NET
-
за копиране на обекти в clipboard в Windows Forms
-
за предаване на обекти по стойност от един домейн на приложение (application domain) в друг
-
за дълбоко копиране на обекти (deep copy)
-
в технологията за отдалечено извикване .NET remoting
Други приложения
След като един обект бъде превърнат в поток от байтове, той може да бъде криптиран, компресиран или обработен по друг начин в съответствие с целта, която сме си поставили. Тези процеси са прозрачни, т.е. не зависят от сериализирания обект. Обектът се сериализира и ние обработваме потока от байтове, без да се интересуваме какви са структурата и съдържанието на обекта. Така сериализацията улеснява обработката на обекти понеже позволява да се запишат в поток.
Защо да използваме сериализация?
Запазването на един обект може да се направи и ръчно, без използването на сериализация. Този подход често е трудоемък и предразполага към допускане на много грешки. Процесът става по-сложен, когато се налага да запазим йерархия от обекти.
Представете си, че изграждате бизнес приложение с 10 000 класа и трябва да запазите сложен граф от навързани един с друг обекти. Представете си как се налага да пишете код във всеки клас, който се справя с протоколи, несъвпадение на типовете при клиент/сървър, управление на грешки, обекти сочещи към други обекти (циклично), работа със структури, масиви и т.н. При по-старите платформи се работеше така, защото нямаше автоматична сериализация.
Сериализацията в .NET е автоматична
Сериализацията в .NET Framework прави целия този процес по обхождането на графа, започващ от даден обект и записването му в поток прозрачен и автоматичен. Тя ни дава удобен механизъм за реализирането на такава функционалност с минимални усилия.
Сериализиране на циклични графи от обекти
С помощта на сериализацията можем да сериализираме циклични графи от обекти, т.е. обекти, които се реферират едни от други. В общия случай съхраняването и предаването на такива структури не е лесно, но в .NET Framework това се реализира от CLR и грижата не е на програмиста. Форматерът сериализира всеки обект само по веднъж и не влиза в безкраен цикъл (форматерите ще обсъдим малко по-нататък в тази тема).
Кратък пример за сериализация?
Следващият фрагмент код илюстрира как можем да сериализираме обект и да го запишем в бинарен файл със средствата на .NET Framework:
string str = ".NET Framework";
BinaryFormatter f = new BinaryFormatter();
using (Stream s = new FileStream("sample.bin", FileMode.Create))
{
f.Serialize(s, str);
}
|
На първия ред е дефиниран обектът, който ще сериализираме. Той може да бъде всякакъв тип – Int32, String, DateTime, Exception, Image, ArrayList, HashTable, потребителски дефиниран клас и т.н. В случая сме използвали обект от тип string. Обектът, който ще бъде сериализиран, трябва да отговаря на специални изисквания, които ще обясним по-нататък в настоящата тема.
За да сериализираме обект, трябва да създадем форматер (formatter). Форматерът е специален клас, който имплементира интерфейса IFormatter. Той извършва цялата работа по сериализирането и десериализирането на йерархия (граф) от обекти и записването им в поток. Сериализирането се извършва от метода Serialize(…). Като първи параметър, този метод очаква наследник на класа System.IO.Stream. Това е потокът, в който ще се сериализират данните, което означава, че обектът може да се сериализира в MemoryStream, FileStream, NetworkStream и т.н. Вторият параметър на метода е обектът, който ще се сериализира.
Потокът, в който ще сериализираме обекта е дефиниран на третия ред в примерния фрагмент код. Използваната using конструкция гарантира затварянето на използвания в нея поток след приключване на работата с него.
Сериализацията на обекта се извършва чрез извикване на метода Serialize(…). В процеса на сериализация се обхождат (чрез reflection) всички член-променливи на обекта и се сериализират само членовете на инстанцията, без статичните й членове. Видимостта на член-променливата няма значение – сериализират се дори private полетата.
Форматери (Formatters)
Форматерите съдържат логиката за записване на резултата от сериализацията в поток, т.е. реализират форматираща логика. Форматерът е клас, който имплементира интерфейса IFormater. Методът му Serialize(…) преобразува обекта до поток от байтове. Методът Deserialize(…) чете данните от потока и пресъздава обекта.
Форматерите съдържат логиката за форматиране на сериализираните обекти. CLR обхожда метаданните за член-променливи и чрез reflection извлича стойностите им. Извлечените стойностите се подават след това на форматера, за да ги запише по подходящ начин в потока.
.NET Framework ни осигурява два стандартни форматера, дефинирани в пространството System.Runtime.Serialization:
-
BinaryFormatter – сериализира обект в двоичен формат. Полученият в резултат на сериализацията поток е много компактен.
-
SoapFormatter – сериализира обект в SOAP формат. За разлика от двоичния формат, SOAP форматът осигурява съвместимост с други системи, защото представлява XML-базиран стандарт за обмяна на съобщения и е независим от платформата. SOAP стандартът ще разгледаме в детайли в темата за уеб услуги. [TODO: link].
Можем да създаваме потребителски дефинирани форматери. Те наследяват абстрактния клас Formatter, осигуряващ базова функционалност.
Процесът на сериализиране
На фигурата схематично е показано как работят процесите на сериализиране и десериализиране в .NET Framework:
При сериализирането на обекта в потока се записват името на класа, името на асемблито (assembly) и друга информация за обекта, както и всички член-променливи, които не са маркирани като [NonSerialized] (употребата на този атрибут ще обясним по-нататък в тази тема). При десериализацията информацията се чете от потока и се пресъздава обектът.
Кратък пример за сериализация
Настоящият пример илюстрира сериализирането на обекти, като се обръща внимание на някои изисквания, на които трябва да отговаря сериализираният обект:
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class FirstExample
{
public int mNumber;
[NonSerialized] public int mId;
public string mName;
}
class Serializer
{
public void Serialize()
{
FirstExample obj = new FirstExample();
BinaryFormatter f = new BinaryFormatter();
using (Stream stream = new FileStream(
"x.bin", FileMode.Create))
{
f.Serialize(stream, obj);
}
}
public void Deserialize() {...}
}
| Как работи примерът?
Нека разгледаме класа FirstExample, който сме дефинирали в примера. Обърнете внимание на атрибута [Serializable], намиращ се преди дефиницията на класа. Приложен към даден тип, този атрибут указва, че инстанциите на типа могат да бъдат сериализирани. При опит за сериализиране на обект, чийто тип няма атрибута [Serializable] CLR предизвиква изключение от тип SerializationException. Допълнително условие, за успешната сериализация на обект е, че всички типове на член-променливите на обекта, които ще бъдат сериализирани, трябва също да притежават атрибута [Serializable].
Обърнете внимание на атрибута [NonSerialized], намиращ се пред декларацията на променливата mId в класа FirstExample. Чрез този атрибут указваме, че съответният член на класа не трябва да бъде сериализиран. Причините да не сериализираме някои от членовете на клас са различни – те може да съдържат секретна информация, която не трябва да бъде съхранявана или да съдържат данни, които не са нужни при пресъздаването на обекта.
Сериализация на обект от дефинирания клас FirstExample, ще извършим във функцията Serialize() на класа Serializer. Първо дефинираме обекта, който ще сериализираме. След това създаваме форматер, който ще извърши работата по сериализацията на обекта. В примера сме използвали форматер от тип BinaryFormatter, който е член на пространството System.Runtime.Serialization.Formatters.Binary. След създаването на форматера, създаваме потока, в който ще бъде сериализиран обекта – в примера сме използвали FileStream. Използваната using конструкция гарантира затварянето на използвания в нея поток след приключване на работата с него. Накрая извикваме функцията Serialize(…) на форматера и обекта се сериализира.
Кратък пример за десериализация
В този пример ще илюстрираме как протича десериализацията на обекти:
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class FirstExample
{
public int mNumber;
[NonSerialized] public int mId;
public string mName;
}
class Serializer
{
public void Serialize(){...}
public void Deserialize()
{
BinaryFormatter f = new BinaryFormatter();
using (Stream stream = new FileStream(
"x.bin", FileMode.Open))
{
FirstExample fe = (FirstExample)
f.Deserialize(stream);
}
}
}
| Как работи примерът?
Този пример е логично продължение на предходния пример за сериализация. В него ще разгледаме метода Deserialize() на класа Serializer, която беше пропусната в предишния пример.
В началото на функцията Deserialize() създаваме форматера, който ще десериализира обекта. Отново използваме BinaryFormatter, понеже такъв тип форматер сме използвали при сериализирането на обекта в предишния пример. След това създаваме потока, от който ще десериализираме обекта. Накрая извикваме функцията Deserialize(…) на форматера, която връща като резултат десериализирания обект. Връщаният тип от функцията Deserialize(…) е System.Object, затова преди да присвоим резултата на променлива от тип FirstExample, трябва да го преобразуваме към този тип.
Бинарна сериализация – пример
Ще представим още един пример за сериализация и десериализация на данни чрез BinaryFormatter:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class Animal
{
private string mDescription;
[NonSerialized] private int mSpeed;
public string Description
{
get
{
return mDescription;
}
set
{
mDescription = value;
}
}
public int Speed
{
get
{
return mSpeed;
}
set
{
mSpeed = value;
}
}
}
class SerializeToFileDemo
{
static void DoSerialization()
{
Animal animal1 = new Animal();
animal1.Description = "One pretty chicken";
animal1.Speed = 3;
Animal animal2 = new Animal();
animal2.Description = "Buggs bunny";
animal2.Speed = 1000;
IFormatter formatter = new BinaryFormatter();
Stream stream =
new FileStream("data.bin", FileMode.Create);
using (stream)
{
formatter.Serialize(stream, animal1);
formatter.Serialize(stream, animal2);
}
}
static void DoDeserialization()
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("data.bin", FileMode.Open);
using (stream)
{
Animal animal1 = (Animal) formatter.Deserialize(stream);
Console.WriteLine("(Description: {0}, Speed: {1})",
animal1.Description, animal1.Speed);
Animal animal2 = (Animal) formatter.Deserialize(stream);
Console.WriteLine("(Description: {0}, Speed: {1})",
animal2.Description, animal2.Speed);
}
}
static void Main()
{
Console.WriteLine("Performing serialization.");
DoSerialization();
Console.WriteLine("Done.\n");
Console.WriteLine("Performing deserialization.");
DoDeserialization();
Console.WriteLine("Done.\n");
}
}
|
След изпълнение на примера, се получава следният резултат:
Как работи примерът?
В началото на примера дефинираме класа Animal. Атрибутът [Serializabe] указва, че инстанциите му могат да бъдат сериализирани. Член-променливата mSpeed е маркирана с атрибута [NonSerialized], поради което не се сериализира.
Класът SerializeToFileDemo съдържа функциите DoSerialization() и DoDeserialization(), които извършват работата по сериализацията и десериализацията на обектите.
Функцията DoSerialization() създава две инстанции на класа Animal, присвоява стойности на полетата им и ги сериализира последователно в двоичен файл, като за целта използва форматер от тип BinaryFormatter.
Функцията DoDeserialization() десериализира сериализираните инстанции и отпечатва полетата им.
При стартиране на програмата се извиква метода DoSerialization() и след това DoDeserialization(), при което стойностите на полетата на сериализираните обекти се отпечатват на екрана. Забележете, че стойността на полето Speed се губи, защото не се сериализира заради атрибута [NonSerialized], който сме използвали в класа Animal.
Сериализация по мрежата – пример
С настоящия пример ще онагледим как можем да сериализираме дървовидна структура от данни с BinaryFormatter и да я пренесем на друг компютър през TCP/IP мрежа.
В примера ще пренасяме животни (инстанции на класа Animal). Примерът се състои от три проекта – изпращач на данни (AnimalSender), получател на данни (AnimalReceiver) и библиотека за типовете, описващи животните (AnimalLibrary). Можем да ги създадем във VS.NET като три отделни проекта в едно и също решение (Solution) или като 2 решения: едното, съдържащо AnimalSender и AnimalLibrary, а другото – AnimalReceiver и AnimalLibrary. В последния случай ще имаме възможност да отворим и да дебъгваме едновременно приложенията за изпращане и за приемане на животни в отделни инстанции на VS.NET като общата част между тях (библиотеката AnimalLibrary) няма да се копира два пъти.
Библиотеката с типове
Библиотеката с типовете, описващи животните, е обща за изпращача и за получателя. Всички типове в библиотеката са отбелязани с атрибута [Serializable], за да се позволи при нужда да бъдат сериализирани от CLR. В нея са дефинирани три типа – Eye, Claws и Animal:
Eye.cs
|
using System;
namespace AnimalLibrary
{
[Serializable]
public class Eye
{
private string mDescription;
private double mDioptre;
public Eye(string aDescription, double aDioptre)
{
mDescription = aDescription;
mDioptre = aDioptre;
}
public override string ToString()
{
string result = String.Format("({0}, {1})",
mDescription, mDioptre);
return result;
}
}
}
|
Класът Eye съдържа две член-променливи – mDescription и mDioptre, които се инициализират от конструктора на класа. В класа е предефиниран метода ToString(), който връща символен низ, описващ съдържанието на обект от този тип.
Claws.cs
|
using System;
namespace AnimalLibrary
{
[Serializable]
public class Claws
{
public string mDescription;
public Claws(string aDescription)
{
mDescription = aDescription;
}
public string Description
{
get
{
return mDescription;
}
}
public override string ToString()
{
return mDescription;
}
}
}
|
Класът Claws съдържа една член-променлива – mDescription, която се инициализира от конструктора на класа. Дефинирано е свойството Description, което е само за четене и връща стойността на член-променливата mDescription. В класа е предефиниран методът ToString(), който връща символен низ, описващ съдържанието на обект от този тип.
Animal.cs
|
using System;
using System.Text;
namespace AnimalLibrary
{
[Serializable]
public class Animal
{
private string mName;
private Claws mClaws;
private Eye[] mEyes;
public string Name
{
get
{
return mName;
}
set
{
mName = value;
}
}
public Claws Claws
{
get
{
return mClaws;
}
set
{
mClaws = value;
}
}
public Eye[] Eyes
{
get
{
return mEyes;
}
set
{
mEyes = value;
}
}
public override string ToString()
{
StringBuilder sbEyes = new StringBuilder(" ");
foreach (Eye eye in mEyes)
{
sbEyes.Append(eye);
sbEyes.Append(" ");
}
string eyesAsString = sbEyes.ToString();
string result =
String.Format("(Name: {0}, Claws: {1}, Eyes: {2})",
mName, mClaws, eyesAsString);
return result;
}
}
}
|
Класът Animal съдържа три член-променливи – mName от тип string, mClaws от тип Claws и mEyes, която е масив от тип Eye. В класа са дефинирани свойства за достъп до член-променливите и е предефиниран метода ToString(), който връща символен низ, описващ съдържанието на обект от този тип.
Защо е нужна библиотеката с типовете?
Библиотеката с типовете е нужна за да могат изпращачът и получателят да работят с един и същ, общ и за двамата, тип, който да прехвърлят през мрежата. Този тип е препоръчително да се намира в общо за двете приложения асембли. Не се препоръчва изпращачът и получателят сами да си дефинират типа, който се прехвърля.
Всъщност последното технически е възможно (от гледна точка на механизмите за сериализация на .NET Framework), но само ако класът, който се сериализира и при изпращача и при получателя е с едно и също име, от един и същ namespace и е дефиниран в асембли със слабо име, което и при изпращача, и при получателя има едно и също име и версия.
|
Препоръчително е когато се сериализират данни и двете страни (сериализиращото приложение и десериализиращото приложение) да работят с един и същ тип, т.е. да ползват общо асембли, в което е дефиниран този тип.
| Приложението-изпращач на данните
Ето как изглежда сорс кодът на приложението, което изпраща инстанции на класа Animal по мрежата към другото приложение, което ги получава:
AnimalSender.cs
|
using System;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using AnimalLibrary;
class AnimalSender
{
const string SERVER_HOSTNAME = "localhost";
const int SERVER_PORT = 10000;
static void Main()
{
Animal animal = new Animal();
animal.Name = "My fluffy cat";
animal.Claws = new Claws("Sharp beautiful claws");
animal.Eyes = new Eye[]
{
new Eye("Left eye", 1.05),
new Eye("Right eye", 0.95)
};
TcpClient tcpClient =
new TcpClient(SERVER_HOSTNAME, SERVER_PORT);
try
{
IFormatter formatter = new BinaryFormatter();
NetworkStream stream = tcpClient.GetStream();
using (stream)
{
formatter.Serialize(stream, animal);
}
Console.WriteLine("Sent animal: {0}", animal);
}
finally
{
tcpClient.Close();
}
}
}
|
Приложението-изпращач създава инстанция на класа Animal, дефиниран в библиотеката AnimalLibrary и инициализира нейните полетата. След това отваря TCP сокет към получателя (чрез класа TcpClient), сериализира инстанцията и я изпраща по сокета. Счита се, че получателят слуша на порт 10 000 на локалната машина (localhost).
Приложението-получател на данните
Нека сега разгледаме и приложението, което посреща сериализираните данни и ги десериализира и използва:
AnimalReceiver.cs
|
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using AnimalLibrary;
class AnimalReceiver
{
const int SERVER_PORT = 10000;
static void Main()
{
TcpListener tcpListener =
new TcpListener(IPAddress.Any, SERVER_PORT);
tcpListener.Start();
Console.WriteLine("Server started.");
while (true)
{
TcpClient client = tcpListener.AcceptTcpClient();
try
{
IFormatter formatter = new BinaryFormatter();
NetworkStream stream = client.GetStream();
using (stream)
{
Animal animal =
(Animal) formatter.Deserialize(stream);
Console.WriteLine("Received animal: {0}", animal);
}
}
finally
{
client.Close();
}
}
}
}
|
Приложението-получател отваря сървърски TCP сокет (на порт 10 000 на локалната машина) и чака за заявки от клиента. Това се извършва с помощта на инстанция на класа TcpListener, чието предназначение е да слуша за връзки от TCP клиенти. При пристигане на заявка от клиента, приложението прочита изпратените от клиента данни и се опитва да ги десериализира в инстанция на класа Animal. След десериализацията, съдържанието на обекта се извежда в конзолата.
Проследяване на примера с VS.NET
За да проследим как се изпълнява примерът, можем да създадем две решения (Solutions) с VS.NET и да ги стартираме.
-
Стартираме VS.NET и създаваме решението AnimalReceiver.sln, което ще представлява сървъра (изпращача на данни). В него създаваме проектите AnimalReceiver.csproj и AnimalLibrary.csproj и копираме в тях съответния им сорс код. Стартираме сървъра с [Ctrl-F5].
-
Стартираме нова инстанция на VS.NET и по същия начин създаваме решението-клиент AnimalSender.sln, което ще посреща изпратените данни. В него създаваме проекта AnimalSender.csproj и добавяме вече създадения проект AnimalLibrary.csproj. Копираме в проекта AnimalSender.csproj сорс кода от неговите класове. Стартираме клиента с [Ctrl-F5] и наблюдаваме прехвърлянето на данни.
При стартирането на приложението-получател, в конзолата се изписва "Server Started.". След стартирането на приложението-изпращач в неговата конзола се получава следният резултат:
Ако се върнем в прозореца на приложението-получател, ще видим, че то е получило правилно изпратения от приложението-изпращач обект от класа Animal:
Дълбоко копиране на обекти – пример
Настоящият пример илюстрира как можем да реализираме дълбоко копиране (deep copy) на обект, използвайки сериализация. Дълбокото копиране не само създава референция, но и клонира всички член-променливи на този обект и всички член-променливи на член-променливите на обекта и т.н. рекурсивно, за да нямат двата обекта нито една обща референция. По принцип създаването на дълбоко копие е нетривиален проблем, но решаването му чрез сериализация е лесно:
using System;
using System.IO;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class SomeClass
{
public StringBuilder mSomeStringBuilder;
public string mSomeString;
public object mSomeObject;
public int mSomeInt;
public SomeClass mSomeClass;
}
class DeepCopyDemo
{
static void Main()
{
SomeClass original = new SomeClass();
original.mSomeString = "Аз съм обикновено стрингче.";
original.mSomeStringBuilder = new StringBuilder(
"Защо този тип ме занимава с тия глупости?!");
original.mSomeObject = new object();
original.mSomeInt = 12345;
original.mSomeClass = original;
SomeClass copy =
(SomeClass) DeepCopyDemo.DeepCopy(original);
Console.WriteLine("copy.mSomeString={0}",
copy.mSomeString );
Console.WriteLine("copy.mSomeStringBuilder={0}",
copy.mSomeStringBuilder);
Console.WriteLine("copy.mSomeObject={0}",
copy.mSomeObject);
Console.WriteLine("copy.mSomeInt={0}\n", copy.mSomeInt );
Console.WriteLine("copy.mSomeClass == copy ? {0}\n",
Object.ReferenceEquals(copy.mSomeClass, copy) );
Console.WriteLine("copy.mSomeClass == original ? {0}\n",
Object.ReferenceEquals(copy.mSomeClass, original) );
Console.WriteLine("Identical instances? {0}",
Object.ReferenceEquals(copy, original));
Console.WriteLine("Equal mSomeString? {0}",
copy.mSomeString == original.mSomeString);
Console.WriteLine("Equal mSomeString by reference? {0}",
Object.ReferenceEquals(copy.mSomeString,
original.mSomeString));
Console.WriteLine("Equal mSomeStringBuilder? {0}",
copy.mSomeStringBuilder == original.mSomeStringBuilder);
Console.WriteLine(
"Equal mSomeStringBuilder.ToString()? {0}",
copy.mSomeStringBuilder.ToString() ==
original.mSomeStringBuilder.ToString());
Console.WriteLine("Equal mSomeObject? {0}",
copy.mSomeObject == original.mSomeObject );
Console.WriteLine("Equal mSomeInt? {0}",
copy.mSomeInt == original.mSomeInt);
}
public static object DeepCopy(object aSourceObject)
{
IFormatter formatter = new BinaryFormatter();
formatter.Context =
new StreamingContext(StreamingContextStates.Clone);
Stream memStream = new MemoryStream();
formatter.Serialize(memStream, aSourceObject);
memStream.Position = 0;
object resultObject = formatter.Deserialize(memStream);
return resultObject;
}
}
| Как работи примерът?
В началото на примера дефинираме класа SomeClass, който е сериализируем и съдържа няколко член-променливи от различни типове, включително и една член-променлива от собствения си тип SomeClass (имаме рекурсивно дефиниран клас). В примера ще направим дълбоко копие на обект от този клас.
В началото на функцията Main() създаваме обект от тип SomeClass и инициализираме член-променливите му със стойности. Забележете, че член-променливата mSomeClass съдържа референция към самия обект.
След инициализирането на член-променливите създаваме копие на обекта, като извикваме функцията DeepCopy(…) на класа. Тя създава дълбоко копие на подадения като параметър обект и връща това копие като резултат от извикването си. За да бъде създадено копието, обектът се сериализира в поток в паметта (MemoryStream) и след това се десериализира в нова инстанция. Член-променливите в десериализираното копие се създават правилно, понеже сериализиращият механизъм на CLR обхожда всички член-променливи и ги сериализира.
След като сме създали дълбоко копие, извеждаме съдържанието на член-променливите му и проверяваме доколко новополученият обект е точно копие на оригиналът. Резултатите от проверките също се извеждат в конзолата.
След изпълнение на примера, се получава следният резултат:
Резултатът показва, че оригиналът и копието, както и всички техни съставни части физически са разположени на различни места в паметта. Те нямат общи референции, т.е. реализирали сме дълбоко копиране на обекта.
IDeserializationCallback
Сериализацията се осъществява лесно, когато сериализираме обекти, които не зависят от други обекти. В реалността често обектите се сериализират заедно, като някои от тях зависят от другите. Това е проблем, понеже при десериализацията не е определен редът, в който се възстановяват обектите. В случаите, когато се налага да знаем кога е завършила десериализацията, за да извършим допълнителни действия върху десериализирания обект, можем да имплементираме интерфейса IDeserializationCallback.
Интерфейсът IDeserializationCallback съдържа един метод, който трябва да имплементираме – OnDeserialization(…). CLR изпълнява този метод след пълната десериализация на обекта. В момента на изпълнение на метода е сигурно, че всички член-променливи са вече десериализирани.
IDeserializationCallback – пример
В настоящия пример ще бъде онагледено използването на интерфейса IDeserializationCallback за извършване на действия след десериализирането на даден обект:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace Demo_4_IDeserializationCallback
{
[Serializable]
class Circle //: IDeserializationCallback
{
private double mRadius;
[NonSerialized]
private double mPerimeter;
[NonSerialized]
private double mArea;
public Circle(double aRadius)
{
mRadius = aRadius;
InitInternalState();
}
private void InitInternalState()
{
mPerimeter = 2 * Math.PI * mRadius;
mArea = Math.PI * mRadius * mRadius;
}
/*
void IDeserializationCallback.OnDeserialization(
object aSender)
{
InitInternalState();
}
*/
public override string ToString()
{
string result= String.Format(
"Radius: {0}, Perimeter: {1}, Area: {2}",
mRadius, mPerimeter, mArea);
return result;
}
}
class IDeserializationCallbackDemo
{
static void Main()
{
Circle circle = new Circle(3.0);
Console.WriteLine("Original circle: {0}", circle);
IFormatter formatter = new BinaryFormatter();
Stream stream = new MemoryStream();
formatter.Serialize(stream, circle);
stream.Position = 0;
Circle newCircle =
(Circle) formatter.Deserialize(stream);
Console.WriteLine("New circle: {0}", newCircle);
}
}
}
| Проследяване на примера
Ако сега стартираме примера, ще получим следния резултат:
Трябва да обърнем внимание на това, че полетата за лице и параметър се губят, защото се сериализира и десериализира само радиусът.
Нека сега премахнем коментарите от заградения с тях код и изпълним отново примера. Този път десериализираният обект е коректно възстановен:
Как работи примерът?
Класът Circle описва геометричната фигура "кръг", която може да се сериализира като се съхрани само радиусът на кръга. Останалите полета са функции на този радиус и не е необходимо да се съхраняват, затова са маркирани с атрибута [NonSerialized].
При десериализирането на обекта е необходимо всички характеристики (полета) на кръга да бъдат възстановени. Това ще бъде извършено от метода IDeserializationCallback.OnDeserialization(…), който се извиква от CLR, след като обектът е създаден изцяло.
В примера се създава обект от тип Circle с определен радиус. Обектът се сериализира, след което се десериализира и съдържанието му се отпечатва в конзолата.
При първото изпълнение на примера, кодът свързан с имплементацията на интерфейса IDeserializationCallback е в коментари, поради което не се извиква функцията, възстановяваща полетата, които не се сериализират. Това е причината полетата за лице и радиус да се губят при десериализацията.
След като премахнем коментарите около кода, свързан с имплементацията на интерфейса IDeserializationCallback и изпълним отново примера, виждаме, че полетата, които не са били сериализирани са възстановени коректно при десериализацията. След като сериализираните променливи са били възстановени и обектът е бил изцяло създаден, е изпълнен методът IDeserializationCallback.OnDeserialization(…), с което са преизчислени лицето и параметъра на кръга.
ISerializable и контролиране на сериализацията
Има случаи, в които се налага да контролираме начина, по който се сериализират обектите. Например може да искаме да намалим обема на съхранената информация за обекта, особено, ако данните се записват във файл. За да предефинираме автоматичната сериализация, трябва да имплементираме интерфейса ISerializable, дефиниран в пространството System.Runtime.Serialization.
Имплементирайки интерфейса ISerializable, трябва да предоставим реализация на метода GetObjectData(…), както и на специален конструктор, който ще бъде използван, когато обектът се десериализира. Те приемат едни и същи параметри – инстанция на класа SerializationInfo и инстанция на структура от тип StreamingContext.
Методът GetObjectData(SerializationInfo, StreamingContext)
При сериализацията на обект от клас, имплементиращ интерфейса ISerializable, форматерът извиква функцията GetObjectData(…). Полетата, които ще бъдат сериализирани, се добавят в SerializationInfo обекта, подаден като параметър на функцията. Това става с помощта на метода AddValue(…) на този обект, който добавя полетата като двойки име/стойност. За име може да бъде използван произволен текст.
Ако нашият клас е наследен от базов клас, които имплементира интерфейса ISerializable, трябва да извикаме base.GetObjectData(info, context), за да позволим на базовия обект да сериализира своите полета.
Конструкторът .ctor(SerializationInfo, StreamingContext)
По време на десериализацията чрез този специален конструктор на класа се подава SerializationInfo обект. За да възстановим състоянието на сериализирания обект, трябва да извлечем стойностите на полетата му от SerializationInfo обекта. Това става чрез имената, които сме използвали при сериализацията на полетата. Ако класът ни наследява клас, имплементиращ интерфейса ISerializable, трябва извикаме базовият конструктор, за да позволим на базовия обект да възстанови своите полета.
|
Не трябва да забравяме да имплементираме този конструктор, защото компилаторът няма как да ни задължи да го направим. Ако забравим да имплементираме конструктора, по време на десериализирането на обекта ще бъде хвърлено изключение.
|
Извличането на стойност от SerializationInfo обект става чрез подаването на името, асоциирано със стойността, на един от GetXXX(…) методите на SerializationInfo, където XXX се заменя с типа на стойността, която ще бъде извлечена - например GetString(…), GetDouble(…) и др.
Контролиране на сериализацията – пример
Настоящият пример илюстрира нагледно, как можем да контролираме сериализацията, имплементирайки интерфейса ISerializable:
using System;
using System.Runtime.Serialization;
[Serializable]
class Person : ISerializable
{
private string mName;
private int mAge;
private Person(SerializationInfo aInfo,
StreamingContext aContext)
{
mName = (string)aInfo.GetString("Person's name");
mAge = aInfo.GetInt32("Person's age");
}
void ISerializable.GetObjectData(SerializationInfo
aInfo, StreamingContext aContext)
{
aInfo.AddValue("Person's name", mName);
aInfo.AddValue("Person's age", mAge);
}
}
| Как работи примерът?
В примера дефинираме класа Person, който е сериализируем и съдържа две член-променливи – mName и mAge, чиито стойности ще запазим при сериализацията. Класът имплементира интерфейса ISerializable, което означава, че ще предостави собствена сериализация на полетата си.
Трябва да маркираме нашия клас с атрибута [Serializable], въпреки че имплементираме интерфейса ISerializable. Без този атрибут CLR не счита, че инстанциите на класа могат да бъдат сериализирани.
Нашият клас имплементира интерфейса ISerializable, затова предоставяме реализация на метода GetObjectData(…) и на конструктора, който ще се извика при десериализацията.
В метода GetObjectData(…) добавяме стойностите на двете полета на класа в SerializationInfo обекта. Това става чрез метода AddValue(…), на който подаваме името, което ще асоциираме със стойността на променливата и самата променлива. Това име ще бъде използвано при десериализацията за извличане на стойността на променливата.
В конструктора на класа извличаме стойностите на променливите от SerializationInfo обекта. За целта използваме имената, които сме асоциирали със стойностите по време на сериализацията им. Прави впечатление, че в примера конструкторът за десериализация е деклариран като private, но това не е грешка, защото CLR може да извиква дори частни конструктори.
Конструкторът и методът GetObjectData(…) приемат като втори параметър StreamingContext обект, указващ къде се сериализира обектът. На StreamingContext структурата ще се спрем по-нататък в тази тема.
Ръчна сериализация с ISerializable – пример
Ще представим още един пример за ръчно сериализиране на обекти в .NET Framework чрез имплементация на интерфейса ISerializable:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
namespace Demo_5_ISerializable
{
[Serializable]
public class Person : ISerializable
{
protected int mAge;
protected string mName;
public Person(string aName, int aAge)
{
mName = aName;
mAge = aAge;
}
protected Person(SerializationInfo aInfo,
StreamingContext aContext)
{
mName = aInfo.GetString("Person's name");
mAge = aInfo.GetInt32("Person's age");
}
public virtual void GetObjectData(SerializationInfo aInfo,
StreamingContext aContext)
{
aInfo.AddValue("Person's name", mName);
aInfo.AddValue("Person's age", mAge);
}
}
[Serializable]
sealed class Employee : Person
{
private string mJobPosition;
public Employee(string aName, int aAge,
string aJobPosition) : base(aName, aAge)
{
mJobPosition = aJobPosition;
}
private Employee(SerializationInfo aInfo,
StreamingContext aContext) : base(aInfo, aContext)
{
mJobPosition = aInfo.GetString("Employee's job");
}
public override void GetObjectData(SerializationInfo aInfo,
StreamingContext aContext)
{
base.GetObjectData(aInfo, aContext);
aInfo.AddValue("Employee's job", mJobPosition);
}
public override string ToString()
{
string value = String.Format(
"(Name: {0}, Age: {1}, Job: {2})",
mName, mAge, mJobPosition);
return value;
}
}
class ISerializableDemo
{
static void Main()
{
Employee employee = new Employee("Jeffrey Richter",
45, "CEO");
Console.WriteLine("Employee = {0}", employee);
FileStream empoyeeFile = new FileStream("employee.xml",
FileMode.Create);
using (empoyeeFile)
{
IFormatter formatter = new SoapFormatter();
formatter.Serialize(empoyeeFile, employee);
Console.WriteLine("Employee serialized.");
empoyeeFile.Seek(0, SeekOrigin.Begin);
Employee deserializedEmployee =
(Employee) formatter.Deserialize(empoyeeFile);
Console.WriteLine("Employee deserialized.");
Console.WriteLine("Deserialized = {0}",
deserializedEmployee);
}
}
}
}
| Как работи примерът?
В примера сме дефинирали клас Person и негов наследник – клас Employee. И двата класа имплементират интерфейса ISerializable и дефинират метод за сериализация GetObjectData(SerializationInfo, StreamingContext), както и конструктор за десериализация със същата сигнатура.
Класът Person e същият като в предишния пример, но сме добавили конструктор, който инициализира полетата му.
Класът Employee има една член-променлива mJobPosition. Първият конструктор служи за инициализация на полета на класа. В него той извиква конструктора на базовия клас и след това инициализира своето поле. Вторият конструктор се използва за десериализация на обекта, като за целта се извиква конструкторът за десериализация на базовия клас и след това се възстановява стойността на член-променливата mJobPosition от подадения SerializationInfo обект. В метода GetObjectData(…) първо се извиква base.GetObjectData(…), за да може базовият клас да съхрани полетата си и след това се съхранява стойността на член-променливата mJobPosition. В класа е предефиниран метода ToString(), който връща символен низ, описващ съдържанието на обект от този тип.
За да демонстрираме работата на сериализацията и десериализацията, във функцията Main() на класа ISerializableDemo създаваме обект от класа Employee и отпечатваме съдържанието му в конзолата. След това създаваме SoapFromatter, с който сериализираме обекта в SOAP формат (ще го разгледаме в детайли в темата за Web услуги [TODO: да се добави линк към темата за Web услуги]) и го записваме във файла employee.xml. Накрая десериализираме сериализирания обект и го отпечатваме в конзолата. Ето какъв е резултатът след изпълнението на примера:
Както виждаме, информацията е възстановена коректно и ръчно реализираните сериализация и десериализация работят успешно. Ето как изглежда и съдържанието на файла employee.xml, в който е записан сериализираният обект:
Имената на XML таговете се вземат от зададените при сериализацията имена, като символите, които не са допустими в имена на тагове се заменят със съответна escaping последователност.
Контекст на сериализация (Streaming Context)
Структурата StreamingContext се използва, за да се укаже къде се сериализира обектът. Тя има две публични свойства:
-
Context – обект асоцииран с инстанция на StreamingContext. Тази стойност обикновено не се използва освен, ако не сме асоциирали интересна стойност с нея в процеса на сериализация.
-
State – стойност от изброимия тип StreamingContextStates. По време на сериализацията това свойство указва къде се сериализира обектът. Например, когато сериализираме във файл, стойността му ще бъде File. По време на десериализация, свойството указва от къде десериализираме данните.
Възможните стойности на StreamingContextStates и техните значения са следните:
-
CrossProcess (0x0001) – данните се сериализират в друг процес на същия компютър.
-
CrossMachine (0x0002) – данните се сериализират на друг компютър.
-
File (0x0004) – данните се сериализират във файл.
-
Persistence (0x0008) – данните се сериализират в база от данни, файл или друг носител.
-
Remoting (0x0010) – данните се сериализират отдалечено на неопределено място, което може да е на друг компютър.
-
Other (0x0020) – не е известно къде се сериализират данните.
-
Clone (0x0040) – указва, че графът от обекти се клонира. Данните се сериализират в същия процес.
-
CrossAppDomain – данните се сериализират в друг домейн на приложение.
-
All (0x00FF) – сериализираните данни могат да са от всеки контекст.
Подавайки StreamingContext обект, форматерът дава информация как ще бъде използван сериализираният обект. Тази информация може да бъде използвана от обекта, за да определи как да сериализира данните си. В зависимост от това къде ще бъде сериализиран, обектът може да сериализира различен брой от полетата си, да направи допълнителна обработка на данните или примерно да хвърли изключение. Не всеки клас има нужда от такава допълнителна обработка, но форматерът ни предоставя необходимата информация и ако ни е нужна, може да я използваме.
За ефективността на сериализацията
Трябва да имаме предвид, че сериализацията е относително бавен процес, тъй като изследва типовете и извлича стойностите им чрез отражение (reflection). Ако трябва да извършваме четене и писане на огромен брой обекти и производителността е от важно значение, се препоръчва да се реализира ръчно записване на стойностите в поток и ръчно възстановяване на обектите вместо да се използва вградената в .NET сериализация. Примерен сценарий, в който е по-добре да се реализира ръчна сериализация е, когато разработваме приложение за мобилно устройство с ограничени ресурси (бавен процесор, малко памет и т.н.).
Сподели с приятели: |