Poznáváme C# a Microsoft.NET 41. díl - pokročilé využití serializace

Po minulé díle, který byl věnován úvodu do použití serializace bych na toto téma dnes rád navázal a to povídáním o dalších způsobech využití tohoto zajímavého mechanismu v rámci .NET.

Selektivní serializace členů

Možná některé z vás, při čtení minulého dílu o serializaci, napadlo jestli není nějakým způsobem možné zajistit selektivní serializaci datových členů. Jinými slovy, jestli lze určit nějaké datové členy, jejichž hodnoty nebudou při serializaci přidány do datového proudu vedoucího k cíli serializace. Obvyklým případem takovýchto datových členů, jsou členy, jejichž hodnoty nemají smysl na jiném počítači či v jiném čase. Dalším případem těchto „přechodných“ členů jsou datové členy, které mohou být vypočteny z jiných datových členů.

Tohoto cíle je nám umožněno v .NET frameworku dosáhnout velmi jednoduše a to použitím atributu System.NonSerialized na konkrétní datový člen. Následující zdrojový kód představuje známou třídu Osoba, která má od minulého dílu navíc datový člen vek, který je pomocí zmíněného atributu označen, aby nebyl serializován.

[Serializable]
public class Osoba
{
 private DateTime datumNarozeni;
 private string jmeno;
 private string prijmeni;
 //datovy clen vek nebude serializovan
 [NonSerialized]
 private int vek;

 public Osoba(string Jmeno, string Prijmeni, DateTime DatumNarozeni)
 {
  this.jmeno = Jmeno;
  this.prijmeni = Prijmeni;
  this.datumNarozeni = DatumNarozeni;
  //vek je vypocitan z datumu narozeni
  this.vek = (DateTime.Now - datumNarozeni).Days/365;
 }
 
 //zbytek tridy
}

To, že datový člen vek není při serializaci přidán do serializačního streamu, logicky znamená, že při deserializaci instance, nemůžeme očekávat obnovení hodnoty členu vek. O tom nás ostatně přesvědčuje následující příklad.

static void DeserializacePriklad()
{
 DateTime lDate = new DateTime(1984,2,9);
 Osoba lOsoba = new Osoba("Jan","Novak",lDate);
 Console.WriteLine("Vek osoby pred serializaci : {0}", lOsoba.Vek);
 IFormatter lFormatter = new BinaryFormatter();
 Stream lStream = null;
 try
 {
  lStream = new FileStream("C:/novak.bin",FileMode.Create);
  lFormatter.Serialize(lStream,lOsoba);
  lStream.Position = 0;
  Osoba lDesOsoba = (Osoba) lFormatter.Deserialize(lStream);
  Console.WriteLine("Vek deserializovane osoby je : {0}", lDesOsoba.Vek);
 }
 finally
 {
  lStream.Close();
 }
}

Zmíněný příklad by měl na vaší obrazovku vytvořit následující výstup:

Vek osoby pred serializaci : 21
Vek deserializovane osoby je : 0

Rozhraní IDeserializationCallback

Věřím, že spoustu z vás po přečtení předchozích řádků napadlo, jestli je správné, že hodnota datového členu vek je nulová, když má být vlastně vypočítána z hodnoty datového členu vypodídajícím o datu narození. Vždyt to přeci znamená porušení takzvaných neměných podmínek třídy. Bylo by tedy dobré mít nějakou možnost mít deserializační proces do určité míry pod kontroulou a správnost neměných podmínek daných třídou zajistit. Dosáhnout této kontroly nad deserializačním procesem instancí dané třídy je možné zajistit implementaci rozhraní System.Runtime.Serialization.IDeserializationCallback.

[Serializable]
public class Osoba : System.Runtime.Serialization.IDeserializationCallback
{
 private DateTime datumNarozeni;
 private string jmeno;
 private string prijmeni;
 [NonSerialized]
 private int vek;

 public Osoba(string Jmeno, string Prijmeni, DateTime DatumNarozeni)
 {
  this.jmeno = Jmeno;
  this.prijmeni = Prijmeni;
  this.datumNarozeni = DatumNarozeni;
  this.vek = (DateTime.Now - datumNarozeni).Days/365;
 }

 #region IDeserializationCallback Members

 public void OnDeserialization(object sender)
 {
  this.vek = (DateTime.Now - datumNarozeni).Days/365;
 }

 #endregion
 //zbytek implementace tridy
}

Zmíněne rozhraní předepisuje jedinou metodu, kterou je OnDeserialization v jejímž tělě můžeme zajistit požadované operaci při deserializačním procesu. V našem případě opět vypočítáme věk instance třídy Osoba, pomocí hodnoty datového členu datumNarozeni, který je v tuto chvíli již deserializován.

Vlastní řízení serializace

Jak již víme, tak každá třída, která má podporovat serializaci, musí být označena atributem Serializable. I když je pomocí atributu NonSerialized a implementace rozhraní IDeserializationCallback možné dosáhnout poměrně solidní kontroly nad serializačním a deserializačním procesem, v některých případech potřebujeme mít kontrolu ještě větší. Za tímto účelem je možné, kromě označení třídy atributem Serializable, nechat třídu implementovat rozhraní System.Runtime.Serialization.ISerializable. To nám umožňuje mimo jiné přesně definovat jaké informace budou serializovány. Pro lepší představu uvažujme následující příklad s mou oblíbenou ukázkovou třídou Zarovka.

[Serializable]
public class Zarovka
{
 private bool sviti;
 private DalsiInfo info = new DalsiInfo();

 public Zarovka(){}

 public DalsiInfo Info
 {
  get{return info;}
  set{info = value;}
 }

 public bool Sviti
 {
  get{return sviti;}
 }

 public void Rozsvitit()
 {
  sviti = true;
 }

 public void Zhasnout()
 {
  sviti = false;
 }
}

//neserializovatelna trida pouzita jako clen typu Zarovka
public class DalsiInfo
{
 private string hodnota;

 public DalsiInfo(){}

 public DalsiInfo(string hodnota)
 {
  this.hodnota = hodnota;
 }
  
 public string Hodnota
 {
  get{return hodnota;}
  set{hodnota = value;}
 }
}

Třída Zarovka evidentně nemůže být serializována, protože má jeden ze svých datových členů typu DalsiInfo, který není serializovatelný. Pokud bychom byli autory třídy DalsiInfo, nebyl by to až takový problém, protože bychom měli možnost tuto třídu jako serializovatelnou jednoduše označit. Představme si, ale situaci, kdy nejsme autory této třídy a tedy možnost označit ji atributem Serializable tím pádem nemáme. A to je právě jeden z případů na použítí rozhraní ISerializable.

Rozhraní definuje metodu GetObjectData příjímající parametry typu SerializationInfo a StreamingContext. Pro nás je zajímavý hlavně první parametr, tedy ten typu SerialzationInfo, pomocí kterého přenášíme potřebná data k serializaci a následně i deserializaci instance. Metoda GetObjectData je volána při serializaci konkrétní implementací IFormatteru. Samozřejmě je také potřeba definovat ještě způsob deserializace, tedy operace, které se provedou při vytváření deserializované instance. To se v případě vlastní serializace učiní implementaci speciálního deserializačního konstruktoru. Nyní se podívejme na řešení našeho problému použitím implementace rozhraní ISerializable třídou Zarovka.

/// <summary>
/// Ukazkova trida na vlastni implementaci serializace
/// </summary>
[Serializable]
public class Zarovka : ISerializable
{
 private bool sviti;
 private DalsiInfo info = new DalsiInfo();

 public Zarovka(){}

 public DalsiInfo Info
 {
  get{return info;}
  set{info = value;}
 }

 public bool Sviti
 {
  get{return sviti;}
 }

 public void Rozsvitit()
 {
  sviti = true;
 }

 public void Zhasnout()
 {
  sviti = false;
 }
 #region ISerializable Members

 public void GetObjectData(SerializationInfo serInfo, StreamingContext context)
 {
  serInfo.AddValue("infoHodnota",info.Hodnota);
  serInfo.AddValue("sviti",sviti);
 }

 #endregion

 //deserializacni konstruktor
 private Zarovka(SerializationInfo serInfo,StreamingContext context)
 {
  this.sviti = serInfo.GetBoolean("sviti");
  this.info = new DalsiInfo(serInfo.GetString("infoHodnota"));
 }
}

V metodě GetObjectData jsou do instance třídy SerializationInfo, pomocí různých přetížených verzi metody AddValue, která pod určitým názvem uchová v páru určitou, hodnotu přidány data, která chceme serializovat. Tato metoda ve všech svých přetížených verzí příjímá jako parametr představující hodnotu nějaký ze základních typů, které jsou všechny označeny jako serializovatelné. Jak jste si jistě všimli, název pod kterým je hodnota uchovávána, nemusí vůbec odpovídat názvu datového členu typu. My jsme v našem příkladu využili tohoto způsobu serializace v tom, že jsme místo pokusu o serializaci celé třídy DalsiInfo serializovali pouze její vnitřní hodnotu, která je typu String, takže již žádné problémy nevznikají. Uchování vnitřní hodnoty nám totiž k zachování stavu této třídy úplně stačí.

Tak jako jsme v metodě GetObjectData specifikovali data k serializaci, tak si v deserializačním konstruktoru, který je při deserializaci vždy volán, z parametru SerializationInfo tato data opět vyzvedneme a provedeme potřebné navázání instančních členů. Takže si pomocí metody GetBoolean vyzvedneme hodnotu atributu sviti a vytvoříme instanci třídy DalsiHodnota s použitím získané vnitřní hodnoty (metoda GetString).

Poznámka :  Při implementaci rozhraní ISerializable je vhodné deserializační konstruktor implementovat s omezenou viditelností, aby jej nemohla zavolat kdejaká třída. A jelikož v přítomnosti nějakého uživatelského konstruktoru .NET framework již nevytváří implicitnítní konstruktor, je často potřeba implicitní konstruktor nadefinovat, aby bylo vůbec možné z uživatelského kódu, instance naší třídy vytvářet.

Zdrojové kódy k příkladům jsou ke stažení zde.

Témata článku: Software, Microsoft, Programování, Public, Private, Info

1 komentář

Nejnovější komentáře

  • Pavel Polívka 24. 11. 2007 13:37:53
    Programovou oflline verzi seriálu naleznete ke stažení na...
Určitě si přečtěte

Monitory do 10 tisíc: poradíme, jaké jsou teď nejlepší

Monitory do 10 tisíc: poradíme, jaké jsou teď nejlepší

** Dobrý monitor s kvalitním panelem lze pořídit pod tři tisíce korun ** Pod deset tisíc si můžete koupit pracovní 27" monitor nebo nejlevnější použitelné 4K ** Vybrali jsme také ideální model pro vícemonitorovou konfiguraci

27.  11.  2016 | Stanislav Janů | 13

Sbíječky vyměnili za klávesnice. Nový projekt má za cíl přeučit horníky na programátory

Sbíječky vyměnili za klávesnice. Nový projekt má za cíl přeučit horníky na programátory

** Programátorů je málo a horníků bez práce po uzavření dolu Paskov bude moc ** Problém řeší unikátní projekt ** Pilotní kurz dává naději, že by z horníků mohli být použitelní kodéři

28.  11.  2016 | David Polesný | 76