1. Einführung
Stell dir einen Wasserkocher mit Wasser vor. Du drehst den Hahn auf – das Wasser beginnt zu fließen. Du kannst viel Wasser auf einmal nehmen und alles auf einmal eingießen, oder du füllst den Wasserkocher Stück für Stück. Genauso ist es mit Dateien – es ist nicht immer praktisch oder möglich, die ganze Datei sofort in den Speicher zu laden. Dateien können groß sein, und manchmal ist die Datenquelle gar keine Datei, sondern zum Beispiel eine Netzwerkverbindung, bei der die Daten nach und nach ankommen.
Wenn wir immer nur mit Byte-Arrays arbeiten würden, wäre bei großen Dateien unser Speicher schnell voll, und für "endlose" Datenströme (wie Video- oder Audiostreams) würde das gar nicht funktionieren. Genau hier hilft das Konzept des Streams!
In .NET ist ein Stream eine Abstraktion für den sequentiellen Zugriff auf Daten: Egal, was die Quelle ist – Datei, Netzwerk, Speicher oder sogar etwas ganz Exotisches wie ein komprimiertes Archiv. Ein Stream erlaubt dir, Daten stückweise zu lesen und zu schreiben, meistens in Blöcken oder Bytes.
Die Hauptidee:
- Stream – das ist ein Kanal für die Datenübertragung. Er ist wie ein Fließband: Du kannst Daten "ablegen" (schreiben) oder "abholen" (lesen), ohne dich direkt um die Details zu kümmern, wo und wie sie gespeichert werden.
- Daten kommen nacheinander: Du kannst das nächste Stück erst lesen, nachdem du das vorherige gelesen hast (oder umgekehrt, wenn Spulen unterstützt wird).
- In den meisten Fällen speicherst du nicht alle Daten auf einmal im Speicher (und dein Computer wird es dir danken).
Diese Abstraktion ist die Grundlage fast aller Ein- und Ausgabeoperationen in .NET: Arbeit mit Dateien, Netzwerken, Archiven, sogar mit der Konsole!
2. Streams System.IO.Stream
Vererbung und Architektur: System.IO.Stream
Fast alle Streams in .NET erben von der abstrakten Klasse System.IO.Stream. Sie definiert die wichtigsten Methoden zum Lesen, Schreiben, Navigieren und Verwalten des Streams.
classDiagram
class Stream {
+Read()
+Write()
+Seek()
+CanRead
+CanWrite
+CanSeek
+Length
+Position
}
class FileStream
class MemoryStream
class NetworkStream
class CryptoStream
Stream <|-- FileStream
Stream <|-- MemoryStream
Stream <|-- NetworkStream
Stream <|-- CryptoStream
- Stream – die abstrakte Basisklasse
- FileStream – für die Arbeit mit Dateien
- MemoryStream – für die Arbeit mit Daten im Speicher
- NetworkStream – für Netzwerkkommunikation
- CryptoStream – für Verschlüsselung/Entschlüsselung
Kurzüberblick über die wichtigsten Eigenschaften und Methoden eines Streams
| Eigenschaft / Methode | Beschreibung |
|---|---|
|
Ob aus diesem Stream gelesen werden kann |
|
Ob in diesen Stream geschrieben werden kann |
|
Ob man im Stream navigieren kann (wird nicht von allen unterstützt) |
|
Länge des Streams (wenn unterstützt – nicht bei allen Streams) |
|
Aktuelle Position im Stream |
|
Daten lesen |
|
Daten schreiben |
|
Im Stream navigieren |
|
Puffer leeren (alles Gesammelte in den Stream schreiben) |
/ |
Stream schließen und Ressourcen freigeben |
Schauen wir uns an, wie das "in der Praxis" aussieht.
3. Beispiel: Dateien lesen und schreiben mit Stream
Hier ein ziemlich minimales Beispiel, um einen Stream "in Aktion" zu sehen:
// Datei zum Schreiben öffnen
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Stell dir vor, wir wollen die Zahlen von 1 bis 10 in die Datei schreiben
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Wir schreiben jeweils ein Byte
}
// Datei explizit schließen, um sie gleich zum Lesen zu öffnen
stream.Close();
// Jetzt versuchen wir, diese Zahlen wieder einzulesen
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Gibt 1, 2, ... 10 aus
}
Hier verwenden wir FileStream, der ein echter Stream im vollen Sinne ist: Du liest und schreibst Daten blockweise oder byteweise.
Arten von Streams: Wo können sie vorkommen?
Ein Stream ist nicht unbedingt nur eine Datei auf der Festplatte. Hier ein paar Beispiele, wo das Stream-Konzept verwendet wird:
- Datei auf der Festplatte (zum Beispiel FileStream – der häufigste Fall)
- Stream im Arbeitsspeicher (MemoryStream – praktisch für temporäre oder Zwischendaten)
- Netzwerkverbindung (NetworkStream)
- Komprimierung/Archivierung (GZipStream, DeflateStream)
- Verschlüsselung (CryptoStream)
- Konsolen-Ein-/Ausgabe (ja, wirklich!) – technisch gesehen auch Streams
Das erlaubt dir, Code zu schreiben, ohne dich um die konkrete Datenquelle oder das Ziel zu kümmern: Wenn dein Code mit einem Stream arbeitet, ist er universell!
4. Nützliche Feinheiten
Lesen und Schreiben sind Operationen, bei denen Daten stückweise übertragen werden. Meistens über Byte-Arrays und die Methoden Read, Write.
Beispiel: Datei blockweise lesen
byte[] buffer = new byte[1024]; // Puffer mit 1024 Bytes (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Verarbeite nur bytesRead Bytes im buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Summe des Blocks: {sum}");
}
Dieser Ansatz wird überall verwendet – von Virenscannern bis zu Musikplayern.
Positionieren im Stream (Position, Seek)
In den meisten Stream-Implementierungen (zum Beispiel bei Dateien) kannst du dich durch die Daten bewegen – du liest nicht einfach das "nächste Stück", sondern kannst zu einer bestimmten Position springen und von dort aus arbeiten.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Springe zum 6. Byte (Nummerierung ab 0)
int value = stream.ReadByte();
Console.WriteLine($"6. Byte in der Datei: {value}");
Streams können nur zum Lesen, nur zum Schreiben oder für beides sein
Manche Streams unterstützen nur eine der Varianten:
- Datei, die zum Schreiben geöffnet wurde: nur Write()
- Stream zum Lesen von Netzwerkdaten: nur Read()
- In manchen exotischen Fällen (z.B. Stream für Druckerausgabe) ist "Zurückspulen" oder Positionieren gar nicht möglich.
Prüfe die unterstützten Operationen mit den Eigenschaften CanRead, CanWrite, CanSeek:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Lesen wird unterstützt");
if (stream.CanWrite)
Console.WriteLine("Schreiben wird unterstützt");
if (stream.CanSeek)
Console.WriteLine("Navigieren in der Datei ist möglich");
Pufferung in Streams
Fast alle Streams verwenden interne Puffer, um die Performance zu verbessern. Pufferung spart Zugriffe auf Festplatte/Netzwerk: Die Daten werden intern gesammelt und dann auf einmal gelesen/geschrieben.
Die Methode Flush() leert den Puffer (zum Beispiel, um sicherzustellen, dass alles auf die Festplatte geschrieben wurde):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hallo, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Garantiert, dass die Daten wirklich auf der Festplatte sind
Wenn du lebenswichtige Daten schreibst (zum Beispiel Zahlungstransaktionen!), ist Flush() dein Freund.
5. Typische Fehler beim Arbeiten mit Streams
Sehr oft machen Anfänger folgende Fehler:
Sie vergessen, den Stream zu schließen (und bekommen Speicherlecks, "hängende" Dateien und anderen Spaß).
Sie verwechseln Text- und Binärstreams – versuchen, einen String mit einer Byte-Methode zu schreiben, und bekommen dann "Kauderwelsch".
Sie verwenden einen zu kleinen Puffer (oder gar keinen) – die Operationen werden langsam.
Sie denken, dass Read() immer genau so viele Bytes liest, wie angefordert – tatsächlich kann es weniger sein; du musst immer den Rückgabewert prüfen.
Sie beachten nicht, dass nicht alle Streams das Navigieren (Seek) unterstützen, besonders Netzwerkstreams.
Zum Beispiel:
// Schlechtes Beispiel: Alle Bytes der Datei lesen, ohne zu prüfen, wie viele tatsächlich gelesen wurden
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead kann kleiner als 1024 sein, wenn die Datei kleiner ist!
}
GO TO FULL VERSION