Сериализация на данни



страница1/2
Дата10.04.2018
Размер405.33 Kb.
#66633
  1   2

Сериализация на данни

Автор


Радослав Иванов

Необходими знания


  • Базови познания за .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 и да ги стартираме.

  1. Стартираме VS.NET и създаваме решението AnimalReceiver.sln, което ще представлява сървъра (изпращача на данни). В него съз­даваме проектите AnimalReceiver.csproj и AnimalLibrary.csproj и копираме в тях съответния им сорс код. Стартираме сър­въра с [Ctrl-F5].

  2. Стартираме нова инстанция на 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 сериализа­ция. Примерен сценарий, в който е по-добре да се реализира ръчна сериализация е, когато разработваме приложение за мобилно устройство с ограничени ресурси (бавен процесор, малко памет и т.н.).

Каталог: dotnetcourse -> Beta1
Beta1 -> Изграждане на графичен потребителски интерфейс с Windows Forms
Beta1 -> Асемблита и разпространение
Beta1 -> Управление на паметта и ресурсите
Beta1 -> Уеб услуги с asp. Net
dotnetcourse -> Кратко ръководство и полезни съвети за това как да разработим своя практически проект
dotnetcourse -> Въпрос: Кога е нужно да слагам в класовете си свойства, и кога мога да слагам публични полета? Не е ли вторият вариант по-удобен, особено когато едно поле не се валидира по никакъв начин? Отговор
Beta1 -> Отдалечени извиквания с. Net remoting


Сподели с приятели:
  1   2




©obuch.info 2024
отнасят до администрацията

    Начална страница