CodeGym /Kurse /C# SELF /Arbeiten mit Byte-Streams

Arbeiten mit Byte-Streams

C# SELF
Level 36 , Lektion 4
Verfügbar

1. Einführung

Wir kennen schon Streams (Stream), die eine Abstraktion für das sequentielle Lesen oder Schreiben von Daten darstellen. Wir haben mit FileStream gearbeitet, um auf Dateien auf Byte-Ebene zuzugreifen, und auch StreamReader und StreamWriter benutzt, um bequem mit Textdaten zu arbeiten – die unter der Haube wiederum FileStream nutzen und sich um Encodings kümmern.

Aber was, wenn wir in einer Datei nicht einfach nur Text speichern wollen, sondern streng typisierte Daten: ganze Zahlen (int), Gleitkommazahlen (double, float), boolesche Werte (bool), Datumswerte (DateTime) oder sogar eigene Strukturen? Klar, man könnte alles in Strings umwandeln und mit StreamWriter speichern und beim Lesen wieder parsen. Aber das hat einige fiese Nachteile:

  • Speicherineffizienz: Die Zahl 12345, als Text gespeichert, braucht 5 Bytes (Zeichen). Im Binärformat braucht ein int nur 4 Bytes. Bei großen Datenmengen macht das einen riesigen Unterschied.
  • Performance: Ständiges Umwandeln von Zahlen in Strings und zurück kostet CPU-Zeit.
  • Datengenauigkeit: Das Umwandeln von Fließkommazahlen in Text und zurück kann zu Genauigkeitsverlusten durch Rundung führen.
  • Parsing-Komplexität: Das manuelle Zerlegen von Textzeilen, um verschiedene Datentypen rauszuziehen (z.B. "123,45 TRUE 2024-06-21"), macht den Code unnötig kompliziert und fehleranfällig.

Für diese Aufgaben gibt es die spezialisierten Klassen BinaryReader und BinaryWriter. Diese Klassen sind spezialisierte Adapter, die auf jedem beliebigen Stream (meistens FileStream) aufsetzen und bequeme Methoden zum Lesen und Schreiben von primitiven C#-Datentypen im Binärformat bieten. Sie übernehmen die ganze Arbeit, Bytes in konkrete Typen umzuwandeln und zurück, und machen das Arbeiten mit strukturierten Binärdateien viel einfacher.

Der springende Punkt: BinaryReader und BinaryWriter sind keine eigenständigen Streams. Sie erweitern die Funktionalität eines bestehenden Stream, indem sie Methoden für C#-Typen statt roher Bytes hinzufügen.

2. Daten schreiben mit BinaryWriter

BinaryWriter bietet eine Reihe von Write()-Methoden, die für jeden primitiven C#-Datentyp überladen sind. Wenn du so eine Methode aufrufst, wandelt BinaryWriter den Wert in seine Binärdarstellung (eine Byte-Sequenz) um und schreibt diese Bytes in den zugrundeliegenden Stream.

Beispiel: Spieleinstellungen speichern

Stell dir vor, wir wollen Spieleinstellungen speichern: Lautstärke (float), aktuelles Level des Spielers (int), ob Musik an ist (bool) und den gewählten Schwierigkeitsgrad (String).


string filePath = "settings.bin";

// 1. FileStream zum Schreiben erstellen
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. BinaryWriter auf FileStream aufsetzen, Encoding für Strings angeben (falls nötig)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. Verschiedene Datentypen schreiben
writer.Write(0.75f);       // float (4 Bytes)
writer.Write(15);          // int (4 Bytes)
writer.Write(true);        // bool (1 Byte)
writer.Write("Easy");      // string (Längenpräfix + Bytes)
                
Console.WriteLine($"Einstellungen gespeichert in '{filePath}'.");

Erklärung zum Beispiel:

  • Wir erstellen einen FileStream mit FileMode.Create, das heißt, es wird eine neue Datei angelegt oder eine bestehende überschrieben.
  • Dann erstellen wir einen BinaryWriter und geben ihm fs. Wichtig: BinaryWriter schließt den zugrundeliegenden Stream (fs) automatisch, wenn Dispose() aufgerufen wird (was durch den using-Block automatisch passiert).
  • Die Methoden writer.Write() sind selbsterklärend: Write(float), Write(int), Write(bool), Write(string). Sie wissen selbst, wie viele Bytes sie für jeden Typ schreiben müssen und wie das Format aussieht.
  • Bei Strings fügt BinaryWriter automatisch einen Längenpräfix vor den eigentlichen Bytes hinzu. So weiß BinaryReader später genau, wie viele Bytes er für den String lesen muss.
  • Wenn du settings.bin in einem Texteditor öffnest, siehst du nur "Müll", weil das eine Binärdatei ist. Zum Anschauen brauchst du einen HEX-Editor.

3. Daten lesen mit BinaryReader

BinaryReader bietet ReadXxx()-Methoden (z.B. ReadInt32(), ReadBoolean(), ReadString()), die die entsprechende Anzahl Bytes aus dem zugrundeliegenden Stream lesen und in den gewünschten C#-Datentyp umwandeln.

Beispiel: Spieleinstellungen laden

Jetzt lesen wir die Einstellungen aus der Datei settings.bin, die wir vorher erstellt haben.


string filePath = "settings.bin";

// 1. FileStream zum Lesen erstellen
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. BinaryReader auf FileStream aufsetzen, gleiches Encoding wie beim Schreiben verwenden
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

// 3. Daten in DERSELBEN REIHENFOLGE lesen, wie sie geschrieben wurden
float volume = reader.ReadSingle();     // float
int level = reader.ReadInt32();         // int
bool isMusicOn = reader.ReadBoolean();  // bool
string difficulty = reader.ReadString(); // string
                
Console.WriteLine($"Einstellungen geladen aus '{filePath}':");
Console.WriteLine($"- Lautstärke: {volume:P0}"); // Als Prozent formatieren (String-Formatierung)
Console.WriteLine($"- Spielerlevel: {level}");
Console.WriteLine($"- Musik an: {isMusicOn}");
Console.WriteLine($"- Schwierigkeit: {difficulty}");

Erklärung zum Beispiel:

  • Wir öffnen den FileStream im FileMode.Open zum Lesen.
  • Wir erstellen einen BinaryReader auf fs und geben dasselbe Encoding an wie beim Schreiben.
  • Extrem wichtig: Die Reihenfolge der reader.ReadXxx()-Aufrufe muss GENAU der Reihenfolge entsprechen, in der die Daten mit BinaryWriter geschrieben wurden. Wenn du z.B. einen string liest, wo ein int geschrieben wurde, gibt's einen EndOfStreamException (wenn der string länger ist) oder du bekommst falsche Daten.
  • Die ReadXxx()-Methoden lesen automatisch die richtige Anzahl Bytes und wandeln sie in den gewünschten Typ um. ReadString() nutzt den Längenpräfix, den BinaryWriter geschrieben hat, um zu wissen, wie viele Bytes zum String gehören.

4. Wichtige Details und Best Practices

Strikte Reihenfolge:

Das ist die wichtigste Regel. BinaryReader und BinaryWriter speichern keine Metadaten über Typen; sie wissen nur, wie viele Bytes jeder primitive Typ braucht. Du musst die Reihenfolge selbst sicherstellen.

Ressourcenmanagement (using):

Wie die meisten .NET-Klassen, die mit Systemressourcen arbeiten (z.B. Dateien oder Netzwerkverbindungen), implementieren auch BinaryReader und BinaryWriter das IDisposable-Interface. Also immer in einen using-Block packen – so wird Dispose() garantiert aufgerufen, auch wenn ein Fehler passiert. Das schützt dich vor Leaks und schließt die Datei sauber.

Übrigens: Standardmäßig rufen BinaryWriter und BinaryReader auch Dispose() für den übergebenen Basis-Stream (z.B. FileStream) auf, der wird also auch automatisch geschlossen.


using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... arbeiten

Encoding für Strings:

Damit das mit Strings, die mit BinaryWriter.Write(string) geschrieben und mit BinaryReader.ReadString() gelesen werden, klappt, unbedingt dasselbe Encoding in den Konstruktoren angeben (z.B. Encoding.UTF8). Sonst gibt's Probleme mit Zeichen außerhalb von ASCII.

Exception-Handling:

Datei-I/O kann immer durch äußere Faktoren gestört werden (Datei fehlt, keine Rechte, Platte voll). Pack deinen Code mit FileStream und BinaryReader/BinaryWriter immer in try-catch-Blöcke, damit dein Programm robust bleibt.

BaseStream und Position:

Du kannst auf den Basis-Stream über die Eigenschaft BaseStream zugreifen (z.B. reader.BaseStream oder writer.BaseStream). Das ist praktisch, wenn du die aktuelle Position wissen willst (BaseStream.Position) oder im File springen willst (BaseStream.Seek()).


// Beispiel für BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(123);
Console.WriteLine($"Aktuelle Position im Stream: {writer.BaseStream.Position}"); // Gibt 4 aus (Größe von int)

writer.Write("Hello");
Console.WriteLine($"Aktuelle Position im Stream: {writer.BaseStream.Position}"); // Gibt 4 + (1+5) = 10 aus

⚠️ Die Methode Write(string) schreibt zuerst die Länge des Strings als 7-Bit-Integer und dann die Bytes des Strings. Die endgültige Größe ist also nicht immer 1 + Länge des Strings.

2
Aufgabe
C# SELF, Level 36, Lektion 4
Gesperrt
Schreiben und Lesen von Ganzzahlen mit BinaryWriter und BinaryReader
Schreiben und Lesen von Ganzzahlen mit BinaryWriter und BinaryReader
1
Umfrage/Quiz
Ein- und Ausgabeströme, Level 36, Lektion 4
Nicht verfügbar
Ein- und Ausgabeströme
Dateien lesen und schreiben
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION