A BinaryReader és BinaryWriter osztályok esetén bemutatásra került, hogy hogyan lehet .NET osztályokat kiírni és beolvasni újra. Az ott bemutatott eljárás működőképes ugyan, de nem éppen a legszebb és komplex osztályok esetén már macerás a használatuk, mert igencsak bonyolódnak a beolvasó és kiíró metódusok.
Éppen ezért a .NET fejlesztésekor gondoltak erre a problémára és készítettek rá megoldást, amit Serialization-nek (magyarul szerializációnak hívjuk, de inkább maradjunk az angol névnél) neveztek el.
A Serialization egyik fajtája a bináris, ami segítségével C# objektumokat tudunk kiírni és beolvasni. Ez leegyszerűsítve úgy működik, hogy a GC által az objektumnak fenntartott memória terület kiírásra, vagy visszaolvasásra kerül, némi logikával megfűszerezve.
Az adatok konzisztenciája, épsége érdekében a kiírt adat tartalmazza a program verzióját, illetve az osztály névterét is, mivel verziók között semmi sem garantálja, hogy ugyanaz az osztály ne változzon. Ez azt jelenti, hogy ha a programunk 1.0 változatában írunk ki egy bináris fájlt, akkor az 1.1 segítségével ugyanazt a fájlt már nem fogjuk tudni beolvasni. Sőt, akkor sem, ha a kiírás után felveszünk extra mezőket az osztályba. Ezért a bináris Serialization segítségével nem célszerű alkalmazás formátumot építeni, viszont kiválóan alkalmazható hálózati kommunikáció során, illetve akkor, ha az adatokra csak ideiglenesen van szükség.
Nézzük meg, hogy bináris Serialization segítségével hogyan egyszerűsíthető a BinaryReader és BinaryWriter esetén használt példa
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace PeldaSerialziationBinary
{
[Serializable]
public class Osztaly
{
public int Egesz { get; set; }
public double Tort { get; set; }
public override string ToString()
{
return $"Egesz: {Egesz}, Tort: {Tort}";
}
}
class Program
{
static void Main(string[] args)
{
BinaryFormatter serializer = new BinaryFormatter();
using (MemoryStream teszt = new MemoryStream())
{
Osztaly kiirando = new Osztaly
{
Egesz = 42,
Tort = 3.1415
};
Console.WriteLine("Írás előtt:");
Console.WriteLine(kiirando);
serializer.Serialize(teszt, kiirando);
//visszaállunk a stream elejére
//az írás után a végén vagyunk
teszt.Seek(0, SeekOrigin.Begin);
Console.WriteLine("Olvasás után: ");
Osztaly beolvasott = (Osztaly)serializer.Deserialize(teszt);
Console.WriteLine(beolvasott);
}
Console.ReadKey();
}
}
}
A program kimenete:
Írás előtt:
Egesz: 42, Tort: 3,1415
Olvasás után:
Egesz: 42, Tort: 3,1415
A példaprogram logikája igencsak egyszerűsödött. Az Osztályunk esetén megjelent definíciója előtt szögletes zárójelekben a Serializable definíció. Ez egy attribútum. Az attribútumok kiegészítő információkkal ruházzák fel a kód egyes részeit, amit a keretrendszer értelmez. ( Ezek használatáról majd részletesen a Reflection esetén lesz szó)
A Serializable attribútum azért kell, hogy megjelöljük, hogy az adott osztály kiírható és visszaállítható.
A Serialization folyamatot a BinaryFormatter osztály végzi. Példányosítása után a Serialize metódussal írjuk ki az adatot egy streambe. Visszaolvasni pedig a Deserialize metódussal tudunk, ami egy object típust ad vissza. Ezt minden esetben konvertálnunk kell a saját típusunkra.
Tételezzük fel, hogy az osztályunkat ki szeretnénk írni, de nem szeretnénk, ha az Egesz tulajdonság része lenne a kiírt adatoknak. Ebben az esetben a NonSerialized attribútummal kell megjelölnünk a tulajdonságot:
[Serializable]
public class Osztaly
{
//A [NonSerialized] csak a közvetlenül követő adattagra lesz értelmezve
[NonSerialized]
public int Egesz { get; set; }
public double Tort { get; set; }
public override string ToString()
{
return $"Egesz: {Egesz}, Tort: {Tort}";
}
}
Buktatók
A NonSerialized attribútum használata minden olyan tulajdonság esetén javasolt, amik számított, futás közben is előállítható értéket tartalmaznak.
A Serializable attribútummal megjelölt osztálynak public módosítóval kell rendelkeznie, hiszen ha belegondolunk private és internal osztályok esetén nem is sok értelme lenne.
A Serializable osztályoknak rendelkezniük kell egy paraméter nélküli konstruktorral. Enélkül nem tudja a keretrendszer létrehozni az objektum példányt visszaállításkor. Ezen felül az adatokat ilyen osztályok esetén tulajdonságok segítségével elérhetővé kell tenni írásra és olvasásra is.
Megjegyzés: .NET 5.0 óta a BinaryFormatter használata elavultnak számít és fordítási figyelmeztetést generál. A keretrendszer későbbi változataiban előfordulhat, hogy a BinaryFormatter eltávolításra kerül, mivel a tapasztalat azt mutatta, hogy a használata több problémát szül, mint amit megold.