1. Einführung
Stell dir vor, du hast mal wieder die Aufgabe, den Inhalt einer Textdatei zu lesen. Sagen wir, du hast eine Datei mit coolen Zitaten von Programmierern, die du in deiner schon bekannten Text-App anzeigen willst. Klar, wir haben schon High-Level-Methoden wie File.ReadAllText gesehen, aber was, wenn die Datei riesig ist und du sie nicht komplett, sondern zeilenweise lesen willst? Oder du willst die Datei kontrolliert lesen, z.B. Zeile für Zeile langsam nachladen, damit dir nicht der Speicher um die Ohren fliegt?
Genau hier kommt StreamReader ins Spiel – eine Klasse, die perfekt dafür ist, Textdateien zeilenweise, blockweise oder sogar Zeichen für Zeichen zu lesen – kurz: flexibel und bequem.
Wie funktioniert StreamReader?
StreamReader ist eine Klasse aus dem Namespace System.IO, die das Lesen von Textdaten aus einem Stream (meistens eine Datei, aber auch Netzwerkstream oder Speicher) ermöglicht. Sie kann Daten in der gewünschten Kodierung lesen, zerlegt sie in Zeichen und Zeilen und gibt dir bequeme Datentypen zurück: Strings und Chars.
Wenn man das als Schema darstellt, sieht das ungefähr so aus:
[ Datei (Bytes) ] --(FileStream)--> [ StreamReader (zerlegt Zeichencodes) ] ---> [ Dein Code (string, char) ]
- Datei (oder andere Byte-Quelle): Gibt uns Bytes.
- FileStream: Liest diese Bytes als "Kanal".
- StreamReader: Wandelt Bytes in Zeichen und Strings um, unter Berücksichtigung der Kodierung (Standard: UTF-8).
Warum sollte man nicht immer nur File.ReadAllText benutzen?
In der Praxis sind Dateien manchmal groß. Richtig groß (z.B. Logs oder CSVs mit Millionen Zeilen). So eine Datei komplett in den Speicher zu laden ist wie einen ganzen Kuchen auf einmal essen zu wollen: lecker, aber gefährlich für die Gesundheit.
StreamReader liest die Datei "stückchenweise". Er lädt nicht die ganze Datei in den Speicher, sondern holt und gibt dir die nächste Zeile oder das nächste Zeichen nur dann, wenn du es brauchst. Das ist sparsam und super praktisch.
2. Datei komplett zeilenweise lesen
Wir erstellen eine Datei quotes.txt mit folgenden Zeilen:
Code – das ist Poesie.
Debuggen – das ist Detektivarbeit.
"Funktioniert nicht" – bester Bug-Report.
Jetzt fügen wir Code zu unserer schon gestarteten Lern-App hinzu (nehmen wir an, das ist ein einfaches Konsolenprogramm, das wir nach und nach erweitern). Wir legen die Datei neben die .exe – das Programm soll sie lesen und den Inhalt zeilenweise ausgeben.
Code mit Kommentaren:
// Pfad zur Datei im Programmordner holen
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
// Prüfen, ob die Datei existiert
if (!File.Exists(filePath))
{
Console.WriteLine($"Datei nicht gefunden: {filePath}");
return;
}
// Datei zum Lesen öffnen, mit StreamReader
using StreamReader reader = new StreamReader(filePath);
string? line;
int lineNumber = 1;
// Datei zeilenweise bis zum Ende lesen
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"{lineNumber,2}: {line}");
lineNumber++;
}
Was passiert hier?
- Wir berechnen den Pfad zu unserer Datei mit Path.Combine und AppDomain.CurrentDomain.BaseDirectory – so läuft der Code gleich auf Windows, Linux und sogar im Docker-Container.
- Erst prüfen wir, ob die Datei wirklich existiert.
- Mit using erstellen wir einen StreamReader und lesen die Datei Zeile für Zeile (ReadLine()).
- Am Ende des using-Blocks wird die Datei geschlossen, auch wenn du das manuell vergisst oder eine Exception gefangen wird.
Visualisierung: Ablauf-Schema
[ quotes.txt ] --(FileStream)--> [ StreamReader ] --(ReadLine)--> [ string line ] --(Console.WriteLine)--> Bildschirm
^
|
AppDomain.CurrentDomain.BaseDirectory + Path.Combine
3. Besonderheiten und Feinheiten
Wie das Lesen von Zeilen funktioniert
Die Methode ReadLine() gibt eine Zeile bis zum ersten Newline-Zeichen (\n oder \r\n) zurück. Wenn die Datei zu Ende ist, kommt null zurück. Deshalb sieht die Schleife meistens so aus:
string? line;
while ((line = reader.ReadLine()) != null)
{
// Zeile verarbeiten
}
Typische Fehler beim Umgang mit StreamReader
Manchmal ist man versucht, using wegzulassen und einfach so zu schreiben:
StreamReader reader = new StreamReader(filePath);
// ...
reader.Close();
So eine Konstruktion ist wie die Tür im Winter offen stehen lassen: Es kann alles Mögliche passieren. Wenn beim Lesen ein Fehler auftritt (z.B. plötzlich kein Zugriff mehr auf die Festplatte), wird der Code zum Schließen der Datei nicht ausgeführt und die Datei bleibt im System offen hängen.
Deshalb gilt: using zu verwenden ist keine Empfehlung, sondern echt Pflicht. Dein zukünftiger Admin-Kollege wird dir danken, dass dein Programm keinen Zoo aus "hängenden" Dateien hinterlässt.
Kurzschreibweise mit using declaration
Mit modernen C#-Versionen (ab 8.0) geht das noch kürzer:
using StreamReader reader = new StreamReader(filePath);
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
Dispose wird am Ende des aktuellen Blocks (der Methode) aufgerufen.
4. Nützliche Feinheiten
Datei mit unbekannter Kodierung lesen
Standardmäßig arbeitet StreamReader mit UTF-8. Aber manchmal gibt es Dateien mit anderer Kodierung (z.B. Windows-1251 für alte russischsprachige Texte). Dann zeigt das Programm "?" oder unlesbare Zeichen an.
Du kannst die Kodierung explizit angeben:
using System.Text;
// Datei als Windows-1251 öffnen
using StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("windows-1251"));
Doku zu Encodings und Encoding
Datei als einen großen Text lesen
Manchmal willst du die ganze Datei als einen Text bekommen, aber nicht mit File.ReadAllText, sondern mit StreamReader.
using StreamReader reader = new StreamReader(filePath);
string allText = reader.ReadToEnd();
Console.WriteLine(allText);
- Die Methode ReadToEnd() liest den gesamten Inhalt der Datei ab der aktuellen Position bis zum Ende als einen String.
- Für große Dateien kann das ineffizient sein und es besteht das Risiko eines OutOfMemoryException. Für Dateien bis ein paar Megabyte ist das aber okay.
Wie das im echten Leben genutzt wird
- Log-Verarbeitung. Wenn du eine Server-Logdatei hast, kannst du sie zeilenweise lesen, Events filtern und musst nicht alles auf einmal in den Speicher laden.
- CSV-Import. Wenn du große Tabellen parsen willst, ist es praktisch, die Daten Zeile für Zeile zu holen und direkt zu verarbeiten.
- Suche nach Schlüsselwörtern in großen Textdateien. Du kannst nach Text in Millionen Zeilen suchen, ohne Angst vor Speicherproblemen.
- Unit-Tests. Testdaten-Dateien werden oft zeilenweise mit StreamReader gelesen.
Vergleich der Lesemethoden
| Methode | Wann benutzen | Vorteile | Nachteile |
|---|---|---|---|
|
Kleine Dateien | Eine Zeile Code, schnell | Große Datei – viel Speicher |
|
Alle, besonders große Dateien | Zeilenweises Lesen, wenig Speicher | Etwas mehr Code, Logik etwas komplexer |
|
Kleine/mittlere Dateien | Kodierung flexibel steuerbar | Schwierig bei sehr großen Dateien |
5. Praxis
Lass uns die Aufgabe schwieriger machen. Unsere App soll nur die ersten N Zeilen der Datei ausgeben, wobei der User die Anzahl eingibt. Manchmal will man ja nicht gleich hunderte Zeilen sehen – ein paar Zitate reichen für die Motivation. :)
So geht das:
using System;
using System.IO;
class Program
{
static void Main()
{
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
if (!File.Exists(filePath))
{
Console.WriteLine($"Datei nicht gefunden: {filePath}");
return;
}
Console.Write("Wie viele Zeilen anzeigen? ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int linesToShow) || linesToShow < 1)
{
Console.WriteLine("Fehler: Gib eine gültige Zahl größer als 0 ein.");
return;
}
using StreamReader reader = new StreamReader(filePath);
int current = 0;
string? line;
while (current < linesToShow && (line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
current++;
}
if (current == 0)
Console.WriteLine("Datei ist leer oder die erste Zeile konnte nicht gelesen werden.");
else if (current < linesToShow)
Console.WriteLine($"Die Datei hatte nur {current} Zeile(n).");
}
}
6. Wichtige Punkte und typische Stolperfallen
Wenn du mit StreamReader arbeitest, ist das Wichtigste: Ressourcen richtig verwalten (siehe vorherige Vorlesung zu IDisposable). Wenn du das vergisst, bekommst du Fehler wie "Datei wird bereits von einem anderen Prozess verwendet" oder "zu viele offene Dateien".
Noch ein Punkt – sei aufmerksam bei der Kodierung. Wenn du nicht sicher bist, dass die Datei UTF-8 ist, gib die gewünschte Kodierung explizit an. Dafür gibt es den zweiten Parameter im StreamReader-Konstruktor.
Eine weitere praktische "Falle" kann sein, wenn die Datei während des Programmablaufs aktualisiert wird (z.B. Log-Datei). Neue Zeilen sieht man nicht immer sofort, wenn die Datei von anderen Prozessen benutzt wird – dann muss man die Datei neu einlesen oder spezielle Methoden nutzen, aber das besprechen wir später.
GO TO FULL VERSION