1. Einführung
Kurz gesagt: ein binäres Serialisierungsformat verwandelt ein Objekt in eine Bytesequenz, die Struktur und Werte so kompakt wie möglich kodiert. Stell dir vor, du beschreibst ein Objekt nicht in Worten (wie bei JSON oder XML), sondern schreibst jedes Bit genau so auf, wie es im Speicher liegt.
In einem Textformat sind die Daten wie ein Brief an einen Freund auf Russisch (jedes Zeichen ist für Menschen verständlich). Im Binärformat ist es eher wie Morsecode, bei dem jeder Punkt und Strich so kurz wie möglich codiert ist und "von Hand" kaum lesbar.
Schemata: Vergleich der Formate
| Format | Menschlich lesbar | Dateigröße | Geschwindigkeit (Schreiben/Lesen) | Kompatibilität |
|---|---|---|---|---|
| XML/JSON | Ja | Groß | Langsamer | Gut |
| Binär | Nein | Klein | Sehr schnell | Begrenzt |
Wie funktioniert binäre Serialisierung in .NET?
Historisch gab es im .NET-Ökosystem das wichtigste Werkzeug für binäre Serialisierung — die Klasse BinaryFormatter. Mit der Weiterentwicklung der Plattform wurde sie aber als unsicher erkannt und aus .NET 9 entfernt. Heute sind andere Wege Standard: BinaryWriter/BinaryReader, und für komplexe Objekte externe Libraries (z.B. protobuf-net).
Kurz zur Historie
BinaryFormatter konnte beliebige mit dem Attribut [Serializable] markierte Klassen nehmen und in Bytes verwandeln, und bei der Deserialisierung die Objektstruktur wiederherstellen. Klingt magisch, bringt aber viele Probleme mit sich (dazu weiter unten).
Moderne Ansätze
Für primitive Typen und einfache Strukturen sind BinaryWriter und BinaryReader praktisch. Für komplexe Objekte eignen sich externe Bibliotheken (z.B. protobuf-net, MessagePack-CSharp u.ä.).
2. Serialisierung von Primitiven mit BinaryWriter
Lass uns unsere Lernanwendung weiterentwickeln. Beispiel: wir wollen Nutzereinstellungen (Benutzername, Punktzahl, Login-Zeit) in einer Binärdatei speichern.
public class UserProfile
{
public string Name { get; set; }
public int Score { get; set; }
public DateTime LoginTime { get; set; }
}
public static Task SaveUserProfile(UserProfile profile, string filePath)
{
// Öffne die Datei zum Schreiben
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new BinaryWriter(stream);
// Schreibe die Daten in Teilen. Zuerst den String, dann die Zahl, dann das Datum
writer.Write(profile.Name ?? string.Empty); // string
writer.Write(profile.Score); // int
writer.Write(profile.LoginTime.ToBinary()); // Datum wird zu "long" konvertiert
}
Und hier das Beispiel zum Lesen:
public static Task<UserProfile> LoadUserProfile(string filePath)
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(stream);
string name = reader.ReadString();
int score = reader.ReadInt32();
long dateData = reader.ReadInt64();
DateTime loginTime = DateTime.FromBinary(dateData);
return new UserProfile { Name = name, Score = score, LoginTime = loginTime };
}
Besonderheiten der primitiven binären Serialisierung
Mit BinaryWriter serialisierst du jedes Property einzeln. Das ist ein verlässlicher und vorhersagbarer Weg: wenn sich die Datenstruktur ändert, siehst du das im Code.
3. Probleme der klassischen binären Serialisierung
Schauen wir auf die Kehrseite. Warum hat Microsoft BinaryFormatter so deutlich missbilligt und sogar seine Benutzung untersagt?
Zerbrechlichkeit des Formats (Schema Evolution Hell)
Binärdaten sind stark an die Klassenstruktur gebunden. Ändert man die Klasse (Feld umbenannt, neues Feld hinzugefügt, altes Feld entfernt) — werden alte Binärdateien unlesbar. Ändert sich die Reihenfolge der Properties — ebenfalls Problem.
Illustration:
// Gestern
public class Profile
{
public string Name;
public int Score;
}
// Heute
public class Profile
{
public string Name;
public double Rating; // Neues Feld hinzugefügt
public int Score;
}
Das Einlesen alter Dateien verursacht entweder Fehler oder liest Felder mit "verrutschten" Daten. Im Gegensatz zu JSON oder XML, wo fehlende Elemente übersprungen werden können, passt sich das Binärformat nicht an — es ist wie eine dichte Betonbahn: ein bisschen daneben gefahren und du fällst direkt vom Fahrrad.
Sicherheitslücken bei Deserialisierung
Das größte Problem von BinaryFormatter ist die potenzielle Verwundbarkeit. Wenn deine Software Binärdaten deserialisiert, die aus einer unzuverlässigen Quelle stammen (z.B. vom Nutzer übers Internet), kann ein Angreifer ein bösartiges "Objekt" einschleusen. In der Vergangenheit führte das sogar zu Remote Code Execution auf der Maschine des Opfers.
Plattformübergreifende Kompatibilität
Ein Binärserializer ist stark an die interne Darstellung von Daten in .NET, an die Runtime-Version, den Compiler und die Architektur (z.B. x64/ARM) gebunden. Serialisierst du unter Windows und versuchst auf Linux zu deserialisieren — Überraschungen garantiert! Sogar zwischen .NET-Versionen können Inkompatibilitäten auftreten.
Schwierige Diagnose
Bei Problemen mit Textformaten kannst du die Datei öffnen, den Inhalt anschauen und meist erahnen, was schiefgelaufen ist. Eine Binärdatei ist ein Rätsel. Alles, was du siehst, ist ein sinnloser Bytestrom. Solche Dateien zu analysieren macht nicht jedem Spaß.
4. Binäre Serialisierung komplexer Objekte
Referenzen
BinaryFormatter konnte Referenzen zwischen Objekten merken (z.B. wenn zwei Properties auf dasselbe Objekt verweisen), aber BinaryWriter und die meisten externen Libraries haben diese Magie nicht. Meist serialisierst du einfach die eingebetteten Objekte nacheinander.
Zyklische Referenzen
Das Serialisieren von Objekten mit zyklischen Verweisen (z.B. wenn die "Mutter" eine Eigenschaft Child hat und das "Kind" eine Eigenschaft Parent zurück auf die Mutter) führt entweder zu einem Fehler oder zu einer Endlosschleife.
Beispiel:
public class Node
{
public Node? Next { get; set; }
public Node? Prev { get; set; }
}
Ein naiver Versuch, dieses Objekt zu serialisieren, führt zur Zyklenbildung.
5. Binäre Serialisierung und Portabilität
Jedes binäre Format (besonders ein selbstgebautes) ist ein "nur für uns"-Format. Wenn du Daten mit anderen Programmen austauschen oder langfristig speichern willst — wähle offene Standards: JSON, XML oder ProtoBuf.
Wann ist binäre Serialisierung gerechtfertigt?
- Wenn die Daten nur innerhalb einer Anwendung leben und nur "kurzzeitig" gespeichert werden.
- Wenn Geschwindigkeit und Kompaktheit wichtig sind (z.B. für große Logs oder Kommunikation zwischen Services innerhalb eines Ökosystems).
- Wenn du beide Seiten strikt kontrollierst: Serialisierung und Deserialisierung.
Alternativen: protobuf, MessagePack und andere
- protobuf-net: Port von Google Protocol Buffers für .NET, geeignet für plattformübergreifenden Austausch und Kompatibilität.
- MessagePack-CSharp: schnelle Implementierung von MessagePack für .NET.
Im Gegensatz zu "rohem" BinaryWriter implementieren diese Bibliotheken Schemata, unterstützen Format-Evolution, Plattformübergreifende Nutzung und Sicherheit. Nutze sie, wenn du auch nur ansatzweise Kompatibilität mit anderen Systemen möchtest.
6. "Manuelle" binäre Serialisierung
Wenn du trotzdem Binärdaten schreiben musst (z.B. in performance-kritischen Anwendungen), benutze BinaryWriter/BinaryReader — und kodier immer explizit Reihenfolge und Datentypen.
Tipps:
- Schreibe die Daten immer in genau der Reihenfolge, in der du sie lesen willst.
- Bei Strukturänderungen pflege eine Versionsnummer oder schreibe einen "magischen Header" (Magic Header).
- Füge vor Strings/Arrays immer deren Länge hinzu.
- Dokumentiere das Dateiformat: sonst verstehst du es in einem Jahr selbst nicht mehr.
Beispiel: Versionierung
// Schreibe die Formatversionsnummer an die erste Stelle
writer.Write((byte)1); // Version 1
writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());
/*
Ermöglicht es, beim Ändern des Formats später Lesebedingungen hinzuzufügen
*/
7. Typische Fehler bei der Arbeit mit binärer Serialisierung
Felder wurden in einer Reihenfolge geschrieben, beim Lesen aber vertauscht. Ergebnis: die Werte "rutschen": ein String wird als int gelesen, ein int als Datum usw.
Du hast 10 Objekte geschrieben, liest aber 11. Der Stream ist dann kaputt: es wird eine Ausnahme wegen EndOfStream auftauchen.
Die Klassenstruktur wurde geändert, alte Binärdateien lassen sich nicht mehr einlesen — die ganze Historie geht verloren.
Du hast vergessen, Exceptions beim Lesen wichtiger Dateien zu behandeln — die Anwendung crasht beim ersten Plattenfehler (z.B. EndOfStreamException).
Du versuchst, Binärdateien ohne klares Format zwischen verschiedenen Programmiersprachen auszutauschen — in 99% der Fälle ist das garantiert schmerzhaft.
Daten werden von unbekannten Nutzern über das Netzwerk deserialisiert — willkommen, Sicherheitslücken! Verwende niemals BinaryFormatter; validiere Eingaben und nutze sichere Formate.
GO TO FULL VERSION