XNA/Tutorials/Serialisierung
Inhaltsverzeichnis
Einleitung
Mir ist aufgefallen, dass viele "Neuling" sehr häufig dem Problem des Speicherns von Daten stehen und diesbezüglich sehr häufig Fragen in diversen Foren gestellt werden. Das habe ich dann gleich mal zum Anlass dieses Tutorials genommen, auch wenn ich mir sicher bin, dass sich viele bereits mit Serialisierung auskennen. Nun wir alle wissen, dass Daten zu speichern ein Zentraler Aspekt von Spielen ist. Wir wollen schließlich nicht nach jedem Neustart des Spiels von ganz vorne beginnen. Vom Speichern der Spieldaten, wie etwa Maps, NPCs usw. mal ganz zu schweigen! Nun jedoch jedes Objekt, welches wir später noch einmal benötigen, von Hand in eine Datei zu schreiben, oder ewig an Methoden zum Lesen und Schreiben der Objekte zu arbeiten, ist aufwändig und im Grunde unnötig.
Theorie
Das "Zauberwort" heisst Serialisierung. Ein Objekt oder einen Objektgraph zu serialisieren bedeutet, das komplexe Konstrukt (das Objekt oder den Objektgraphen) in einen einfachen Bytestrom zu wandeln, um diesen bspw. als eine Datei auf der Festplatte abzulegen, ihn über ein Netzwerk zu verschicken oder einfach eine Kopie des Objekts zu erstellen. Dabei wird aber immer nur der Zustand eines Objekts (oder Objektgraphen) umgewandelt, Methoden nicht. Grundsätzlich gibt es zwei verschiedene Arten der Serialisierung, die Oberflächliche Serialisierung (Shallow Serialization) und die tiefe Serialisierung (Deep Serialization). Der Unterschied besteht darin, dass bei der oberflächlichen Serialisierung lediglich öffentliche Eigenschaften eines Objekts serialisiert werden. Private oder geschützte Eigenschaften sowie komplette Objektgraphen werden nicht serialisiert, dazu bedarf es der tiefen Serialisierung. Methoden werden, wie bereits erwähnt, von keiner der beiden Serialisierungsarten mit in den Bytestrom umgewandelt.
Hier noch zwei kleine Illustrationen, die den Abbildungen aus einem Buch von Alex Bierhaus und Jürgen Kotz nachempfunden sind.
Die Formatierer
Kommen wir nun zu den verschiedenen Formatierern. Formatierer sind jene Klassen im .Net Framework, welche uns die nötigen Methoden zum Serialisieren eines Objekts zur Verfügung stellen und es gibt gleich drei von ihnen im .Net Framework.
Der XMLSerializer:
Dieser Formatierer unterstützt lediglich die Shallow Serialization. Wie der Name bereits vermuten lässt, wird der Bytestrom in das leicht les- und editierbare XML-Format geschrieben. Zufinden ist er im Namespace 'System.Xml.Serialization'
Der SOAP-Formatter:
Dieser Formatter unterstützt die Deep Serialization. Auch hier wird der Bytestrom in ein XML-Format geschrieben, jedoch innerhalb eines sogenannten SOAP-Bodys innerhalb eines SOAP-Envelopes. Für weitere Informationen zum SOAP-Envelope ist wie immer Wikipedia euer Freund: http://de.wikipedia.org/wiki/SOAP
Eines sei jedoch noch gesagt, der SOAP-Formatter unterstützt keine Generics. Anstatt von List<Type> müsst ihr also auf eine ArrayList zurückgreifen. Zufinden ist dieser Formatierer im Namespace 'System.Runtime.Serialization.Formatters.Soap'. Um ihn nutzen zu können müsst ihr eurem Projekt einen Verweis auf die "System.Runtime.Serialization.Formatters.Soap.dll" hinzufügen.
Der Binary-Formatter:
Auch dieser Formatierer unterstützt die Deep Serialization, der Bytestrom wird jedoch NICHT in das lesbare XML-Format geschrieben. Meist ist der Bytestrom kompakter, als der des SOAP-Formatters und außerdem unterstützt er Generics.
Der Code
Nach all dem Text möchte ich nun noch kurz ein wenig Code hinzufügen, um zu zeigen, wie man eine einfache Klasse Serialisiert. Zunächst aber zu der Klasse selber
Die Klasse
[Serializable] public class Angestellter { public string Name; private int Alter; private string Abteilung; [NonSerialized] public object UnnötigeInformation; public Angestellter(string name, int alter, string abteilung) { Name = name; Alter = alter; Abteilung = abteilung; } }
Alles in Allem handelt es sich hierbei um eine ziemlich einfache Klasse, mit einem Konstruktor, zwei öffentlichen und zwei privaten Variablen. Was jedoch auffallen dürfte ist das "[Serializable]" Attribut der Klasse. Jedes Objekt, egal ob Teil eines Objektgraphen oder nicht, das man serialisieren möchte muss mit diesem Attribut ausgestattet werden, sonst erhält man eine Exception. Auch auffallen dürfte das "[NonSerialized]" Attribut der öffentlichen Variable "UnnötigeInformation". Alles, was mit diesem Attribut versehen ist, wird bei der serialisierung vom Formatierer nicht beachtet. Es ist also völlig egal, welchen Formatierer ich wähle, "UnnötigeInformationen" wird niemals serialisiert.
Die Serialisierung
So, jetzt aber zur eigentlichen Serialisierung des Objekts. Ich habe mir dazu zwei Buttons auf einer Form erstellt, die ich zur Serialisierung und Deserialiserung verwende. Zur Serialisierung verwenden wir folgenden Code:
// Neues Objekt vom Typ Angestellter erstellen Angestellter Arbeiter01 = new Angestellter("Max Mustermann", 33, "Verwaltung"); // Neuen Binary-Formatierer erstellen System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); // Neuen FileStream, zu dem serialisiert werden soll, erstellen System.IO.FileStream fs = new System.IO.FileStream("C:\\Arbeiter01.bin", System.IO.FileMode.OpenOrCreate); // Objekt "Arbeiter01" serialisieren bf.Serialize(fs, Arbeiter01); // FileStream schließen und freigeben fs.Close(); fs.Dispose();
Wie man sieht habe ich mich für den Binary-Formatierer entschieden. Indem ich die "Serialize"-Methode des Formatierers aufrufe und sowohl einen Filestream, als auch mein zu serialisierendes Objekt übergebe, wird mein Objekt vom Typ "Angestellter" serialisiert. Der Rest des Codes sollte anhand der Kommentare selbsterklärend sein.
Die Deserialisierung
Nun, jetzt haben wir das Objekt serialisiert, aber was haben wir davon, wenn wir sie nicht wieder einlesen können?! Genau, NICHTS! Kommen wir also zum Einlesen der Datei, dem sogenannten Deserialisieren. Dazu nutzen wir diesen Code:
// Objekt vom Typ Angestellter erstellen Angestellter Arbeiter01; // Neuen Binary-Formatierer erstellen System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); // Neuen FileStream, zu dem serialisiert werden soll, erstellen System.IO.FileStream fs = new System.IO.FileStream("C:\\Arbeiter01.bin", System.IO.FileMode.OpenOrCreate); // Objekt deserialisieren (Cast nicht vergessen) Arbeiter01 = (Angestellter)bf.Deserialize(fs); // FileStream schließen und freigeben fs.Close(); fs.Dispose(); // Name des deserialisierten Objekts ausgeben MessageBox.Show(Arbeiter01.Name);
Zum deserialisieren muss ich natürlich den selben Formatierer wählen, den ich zum Serialisieren verwendet habe. Also kommt auch hier wieder der Binary-Formatierer zum Zuge. Zum deserialisieren rufen wir dann die "Deserialize"-Methode des Binary-Formatieres auf und übergeben dem Ganzen einen FileStream. Die Methode wird uns nun ein Objekt vom Typ "object" zurückgeben. Aus diesem Grund müssen wir noch einen Cast (in diesem Fall vom Typ Angestellter) ausführen! Der Rest ist, denke ich mal, wieder selbsterklären.
Schlusswort
Somit haben wir ein Objekt serialisiert und auch wieder deserialisiert! Wie man sieht ist das Ganze in der Praxis ganz simpel, aber überaus mächtig. Eine kleine Einschränkung gibt es allerdings innerhalb des .Net Frameworks! Da alle Controls (Buttons, Textboxen, Forms usw.) NICHT mit dem "[Serializable]" Attribut versehen sind, lassen sich diese auch nicht serialisieren. (Zumindest nicht ohne ein paar Umwege)

