## 12.4 Die Klassen »TextReader« und »TextWriter« Wie Sie in den vorhergehenden Abschnitten gesehen haben, stellt die Klasse Stream Operationen bereit, mit denen Sie unformatierte Daten byteweise lesen und schreiben können. Stream-Objekte bieten sich daher insbesondere für allgemeine Operationen an, beispielsweise für das Kopieren von Dateien. Die Klasse Stream beziehungsweise die daraus abgeleiteten Klassen sind aber weniger gut für textuelle Ein- und Ausgabeoperationen geeignet. Um den üblichen Anforderungen von Textoperationen zu entsprechen, stellt die .NET-Klassenbibliothek die beiden abstrakten Klassen TextReader und TextWriter bereit. Objekte, die aus der Klasse Stream abgeleitet werden, unterstützen den vollständigen Satz an E/A-Operationen, also sowohl das Lesen als auch das Schreiben. Nun wird die Bearbeitung auf zwei Klassen aufgeteilt, die entweder nur lesen oder nur schreiben können. TextReader und TextWriter sind abstrakt definiert und müssen daher abgeleitet werden. Das .NET Framework bietet solche Ableitungen mit StreamReader und -Writer sowie StringReader und -Writer an. Von TextWriter gibt es auch noch weitere, spezialisierte Ableitungen. Im Folgenden werden wir uns mit den Klassen StreamReader und StreamWriter beschäftigen. Hier klicken, um das Bild zu vergrößern Abbildung 12.4 Die Objekthierarchie der »Reader«- und »Writer«-Klassen ### 12.4.1 Die Klasse »StreamWriter« #### Konstruktoren der Klasse »StreamWriter« Wir werden uns daher zunächst einigen Konstruktoren der Klasse StreamWriter zuwenden, um zu sehen, auf welcher Basis sich ein Objekt dieses Typs erzeugen lässt.
    public StreamWriter(Stream);
    public StreamWriter(string);
    public StreamWriter(Stream, Encoding);
    public StreamWriter(string, bool);
    public StreamWriter(Stream, Encoding, int);
    public StreamWriter(string, bool, Encoding);
    public StreamWriter(string, bool, Encoding, int);
Es fällt zunächst auf, dass wir jedem Konstruktor entweder eine Zeichenfolge oder ein Objekt vom Typ Stream übergeben müssen. Entscheiden wir uns für eine Zeichenfolge, enthält diese die Pfadangabe zu einer Datei. Da die Klasse Stream abstrakt ist, können wir natürlich keine Referenz auf ein konkretes Stream-Objekt übergeben. Aber die Klasse Stream wird abgeleitet, beispielsweise von FileStream. Die Referenz auf ein Objekt einer aus Stream abgeleiteten Klasse gilt aber nach den Paradigmen der Objektorientierung gleichzeitig als ein Objekt vom Typ der Basisklasse. Also kann dem Parameter im Konstruktor, der den Typ Stream erwartet, ein Objekt vom Typ einer aus Stream abgeleiteten Klasse übergeben werden. Nun sehen wir uns natürlich sofort mit der Frage konfrontiert, welchen Sinn es hat, ein Stream-Objekt als Argument an den Konstruktor zu übergeben. Wie Sie sich vielleicht noch erinnern, werden die Stream-Objekte generell in zwei Typen klassifiziert: in Base-Streams und Pass-Through-Streams. Ein Base-Stream endet zum Beispiel direkt in einer Datei oder in einer Netzwerkverbindung, ein Pass-Through-Stream ist ein »Durchlaufobjekt«, das die Fähigkeiten eines Base-Streams erweitert. Betrachten wir zunächst den Konstruktor der Klasse StreamWriter, der in einem String eine Pfadangabe entgegennimmt:
    public StreamWriter(string);
Ein Objekt, das basierend auf dieser Erstellungsroutine instanziiert wird, weiß, wohin die Daten geschrieben werden – nämlich in die Datei, die durch das String-Argument beschrieben wird, zum Beispiel:
    StreamWriter myStreamWriter = new StreamWriter(@"D:\MyText.txt");
Wir erzeugen mit dieser Anweisung einen Base-Stream, der die Daten – genauer gesagt eine Zeichenfolge – in eine Datei schreiben kann. Nun wollen wir ein anderes StreamWriter-Objekt erzeugen, diesmal allerdings auf Basis der Übergabe eines FileStream-Objekts.
    FileStream fs = new FileStream(@"D:\Test.txt", FileMode.CreateNew);
    StreamWriter myStreamWriter = new StreamWriter(fs);
In der ersten Anweisung wird ein Objekt vom Typ FileStream erstellt, das eine neue Datei namens Test.txt in der Root D:\ erzeugt. Dieses Objekt wird seinerseits als Argument an den Konstruktor der Klasse StreamWriter übergeben. Als Resultat liegt eine Hintereinanderschaltung von zwei Stream-Objekten vor, woraus sich Nutzen ziehen lässt. Wie Sie wissen, schreiben und lesen Objekte, die auf der Stream-Klasse basieren, nur elementare Bytes. Demgegenüber schreiben StreamWriter-Objekte Zeichen mit einer speziellen Verschlüsselung (Encoding) in den Datenstrom. Sie arbeiten im Endeffekt mit einem Datenstrom, der die Charakteristika beider Datenflüsse kombiniert. In ähnlicher Weise könnten Sie natürlich auch einen MemoryStream oder NetworkStream als Argument übergeben. Standardmäßig verschlüsselt StreamWriter nach UTF-8, eine Abweichung davon wird durch die Wahl eines Konstruktors erreicht, der einen Parameter vom Typ Encoding aus dem Namespace System.Text entgegennimmt. Sie können hier beispielsweise ein Objekt vom Typ UTF7Encoding oder UnicodeEncoding (entspricht der UTF-16-Kodierung) übergeben.
Anmerkung
Schreiben wir Zeichen in einen Stream, müssen die Bytes in bestimmter Weise interpretierbar sein. Standardmäßig wird in Mitteleuropa zur Kodierung der ANSI-Zeichensatz (Codeseite 1252) benutzt, der Zeichencodes zwischen 0 und 255 zulässt und unter anderem auch Sonderzeichen wie »ä«, »ö« und »ü« beschreibt. Damit unterscheidet sich der ANSI-Zeichensatz vom ASCII-Zeichensatz, der nur die Codes von 0 bis 127 festlegt. Um einen Text korrekt zu übertragen und anzuzeigen, dürfte streng genommen nur der ASCII-Zeichensatz verwendet werden, weil nur die Codes 0–127 unter ANSI und ASCII identisch sind. Um Probleme dieser Art zu vermeiden, wurde mit Unicode ein neuer Zeichensatz geschaffen. Allerdings hat auch Unicode unterschiedliche Formate, denn es wird zwischen UTF-7, UTF-8, UTF-16 und UTF-32 unterschieden. Der UTF-8-Zeichensatz ist wohl der wichtigste, denn er ist der Standard unter .NET. In diesem Zeichensatz werden Unicode-Zeichen in einer unterschiedlichen Anzahl Bytes verschlüsselt. Die ASCII-Zeichen werden in einem Byte gespeichert, alle anderen Zeichen in weiteren zwei bis vier Byte. Das hat den Vorteil, dass Systeme, die nur ASCII- oder ANSI-Zeichen verarbeiten, mit der UTF-8-Kodierung klarkommen.
Einige Konstruktoren erwarten zusätzlich einen booleschen Wert. Dieser kommt nur im Zusammenhang mit den Konstruktoren vor, die in einer Zeichenfolge die Pfadangabe zu der Datei erhalten, in die der Datenstrom geschrieben werden soll. Mit true werden die zu schreibenden Daten an das Ende der Datei gehängt – vorausgesetzt, es existiert bereits eine Datei gleichen Namens in dem Verzeichnis. Mit der Übergabe von false wird eine existierende Datei überschrieben. Der letzte Parameter, der Ihnen in zwei Konstruktoren zur Verfügung steht, empfängt einen Wert vom Typ int, mit dem Sie die Größe des Puffers beeinflussen können. #### Das Schreiben in den Datenstrom Schauen wir uns zunächst ein Codefragment an, mit dem wir eine Datei erzeugen, in die wir den obligatorischen Text »Visual C# macht Spaß!« schreiben:
    StreamWriter sw = new StreamWriter(@"D:\NewFile.txt");
    sw.WriteLine("Visual C#");
    sw.WriteLine("macht Spaß!");
    sw.Close();
Einfacher geht es nicht mehr! Zunächst wird ein Konstruktor aufgerufen, und diesem wird zur Initialisierung des StreamWriter-Objekts eine Zeichenkette als Pfadangabe übergeben. Daraufhin wird entweder die Datei erzeugt oder eine existierende gleichnamige Datei im angegebenen Verzeichnis überschrieben. Mit jedem Aufruf der von TextWriter geerbten Methode WriteLine wird eine Zeile in die Datei geschrieben und ihr am Ende ein Zeilenumbruch angehängt. Mit unserem Codefragment erzeugen wir also eine zweizeilige Textdatei. Es liegt die Vermutung nahe, dass StreamWriter eine zweite Methode zum Schreiben in den Datenstrom bereitstellt, die ohne den automatisch angehängten Zeilenumbruch in den Strom schreibt. Ein Blick in die Klassenbibliothek bestätigt die Vermutung: Es gibt eine Methode Write. Diese Methode ist genauso überladen wie die Methode WriteLine. Write und WriteLine bilden den Kern der Klasse StreamWriter. Viel mehr Methoden hat die Klasse auch nicht anzubieten, denn alle anderen sind bereits gute Bekannte: Close, um einen auf dieser Klasse basierenden Strom zu schließen, und Flush, um die Daten, die sich im Puffer befinden, in den Strom zu schreiben und den Puffer zu leeren. Tabelle 12.17 gibt die wichtigsten Methoden eines StreamWriter-Objekts wieder.

Tabelle 12.17 Methoden eines »StreamWriter«-Objekts

Methode | Beschreibung
Close | Schließt das aktuelle Objekt sowie alle eingebetteten Streams.
Flush | Schreibt die gepufferten Daten in den Stream und löscht danach den Inhalt des Puffers.
Write | Schreibt in den Stream, ohne einen Zeilenumbruch anzuhängen.
WriteLine | Schreibt in den Stream und schließt mit einem Zeilenumbruch ab.
#### Eigenschaften der Klasse »StreamWriter« Mit AutoFlush veranlassen Sie, dass Daten aus dem Puffer in den Datenstrom geschrieben werden, sobald eine der Write/WriteLine-Methoden aufgerufen wird und diese Eigenschaft auf true gesetzt ist. Wollen Sie das aktuelle Textformat erfahren, können Sie die Eigenschaft Encoding auswerten:
    StreamWriter sw =
        new StreamWriter(@"C:\NewFile.txt", false, Encoding.Unicode);
    Console.WriteLine("Format: {0}", sw.Encoding.ToString());
Als dritte und letzte Eigenschaft steht Ihnen noch BaseStream zur Verfügung, die das Objekt des Base-Streams liefert, auf dem das StreamWriter-Objekt basiert.

Tabelle 12.18 Die Eigenschaften der Klasse »StreamWriter«

Eigenschaften | Beschreibung
AutoFlush | Löscht den Puffer nach jedem Aufruf von Write oder WriteLine.
BaseStream | Liefert eine Referenz auf den Base-Stream zurück.
Encoding | Liefert das aktuelle Encoding-Schema zurück.
### 12.4.2 Die Klasse »StreamReader« Die aus der Klasse TextReader abgeleitete Klasse StreamReader ist das Gegenstück zur Klasse StreamWriter. Betrachtet man ihre Möglichkeiten, sind die Klassen praktisch identisch – abgesehen von der Tatsache, dass das charakteristische Merkmal dieser Klasse in der Fähigkeit besteht, Daten einer bestimmten Kodierung aus einem Strom zu lesen. Die Konstruktoren ähneln denen der Klasse StreamWriter. Sie nehmen im einfachsten Fall die Referenz auf einen Stream oder eine Pfadangabe als String entgegen. Sie gestatten aber auch, die eingelesenen Zeichen nach einem durch Encoding beschriebenen Schema zu interpretieren oder die Puffergröße zu variieren. Tabelle 12.19 enthält die wichtigsten Methoden eines StreamReaders.

Tabelle 12.19 Methoden der Klasse »StreamReader«

Methode | Beschreibung
Peek | Liest ein Zeichen aus dem Strom und liefert den int-Wert zurück, der das Zeichen repräsentiert, verarbeitet das Zeichen aber nicht. Der Zeiger wird nicht auf die Position des folgenden Zeichens gesetzt, wenn Peek aufgerufen wird, sondern verbleibt in seiner Stellung. Verweist der Zeiger hinter den Datenstrom, ist der Rückgabewert –1.
Read | Liest ein oder mehrere Zeichen aus dem Strom und liefert den int-Wert zurück, der das Zeichen repräsentiert. Ist kein Zeichen mehr verfügbar, ist der Rückgabewert –1. Der Positionszeiger verweist auf das nächste zu lesende Zeichen. Eine zweite Variante dieser überladenen Methode liefert die Anzahl der eingelesenen Zeichen.
ReadLine | Liest eine Zeile aus dem Datenstrom – entweder bis zum Zeilenumbruch oder bis zum Ende des Stroms. Der Rückgabewert ist vom Typ string.
ReadToEnd | Liest von der aktuellen Position des Positionszeigers bis zum Ende des Stroms alle Zeichen ein.
Wir wollen nun an einem Codebeispiel das Lesen aus einem Strom testen.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 12\StreamReaderSample
    // ---------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        // Datei erzeugen und mit Text füllen
        StreamWriter sw = new StreamWriter(@"D:\MyTest.kkl");
        sw.WriteLine("Visual C#");
        sw.WriteLine("macht Spaß.");
        sw.Write("Richtig??");
        sw.Close();
        // Datei an der Konsole einlesen
        StreamReader sr = new StreamReader(@"D:\MyTest.kkl");
        while(sr.Peek() != -1)
          Console.WriteLine(sr.ReadLine());
        sr.Close();
        Console.ReadLine();
      }
    }
Zunächst wird mit einem StreamWriter-Objekt eine Datei mit dem Namen MyTest.kkl erzeugt. Die Dateierweiterung ist frei gewählt, sie muss nicht zwangsläufig .txt zur Kennzeichnung als Textdatei lauten. Wichtig ist nur, die Daten der Datei beim späteren Lesevorgang richtig zu interpretieren. Solange wir wissen, dass wir es mit einer Textdatei zu tun haben, bereitet uns eine individuelle Dateierweiterung keine Probleme. In den Datenstrom sw vom Typ StreamWriter werden drei Textzeilen geschrieben. Danach darf man nicht vergessen, den Strom wieder zu schließen, denn ansonsten wird man mit einer Fehlermeldung konfrontiert, wenn nachfolgend der Versuch unternommen wird, die Datei zum Lesen zu öffnen. Um den Dateiinhalt zu lesen, nutzen wir ein Objekt vom Typ StreamReader, dessen Konstruktor wir den Pfad zu der Datei übergeben. Mit der ReadLine-Methode wird Zeile für Zeile aus dem Strom gelesen. Um den Lesevorgang zum richtigen Zeitpunkt wieder zu beenden, müssen wir das Ende der Datei feststellen. Hierbei ist die Methode Peek behilflich, deren Rückgabewert –1 ist, wenn der Zeiger auf die Position hinter dem Ende des Stroms verweist. Dieses Verhalten machen wir uns zunutze, indem wir daraus die Abbruchbedingung der Schleife formulieren. In der while-Schleife werden so lange mit der ReadLine-Methode des StreamReader-Objekts Zeilen aus dem Datenstrom geholt (und dabei automatisch der Zeiger auf das nächste einzulesende Zeichen gesetzt), bis die Abbruchbedingung erfüllt wird, das heißt, Peek –1 zurückliefert. Die Ausgabe an der Konsole wird wie folgt lauten:
    Visual C#
    macht Spaß.
    Richtig??
Da wir die komplette Textdatei auslesen wollen, könnten wir auch einen einfacheren Weg gehen und die komplette while-Schleife gegen die folgende Programmcodezeile austauschen:
    Console.WriteLine(sr.ReadToEnd());