## 12.3 Die »Stream«-Klassen Ein Stream ist die abstrahierte Darstellung eines Datenflusses aus einer geordneten Abfolge von Bytes. Welcher Natur dieser Datenstrom ist – ob er aus einer Datei stammt, ob er die Eingabe eines Benutzers an der Tastatur enthält oder ob er möglicherweise aus einer Netzwerkverbindung bezogen wird –, das bleibt zunächst einmal offen. Die Beschaffenheit des Datenflusses hängt nicht nur von Sender und Empfänger ab, sondern auch ganz entscheidend vom Betriebssystem. Ein Entwickler soll seine Aufgabe unabhängig von diesen spezifischen Details lösen. E/A-Streams werden deshalb von Klassen beschrieben, die Allgemeingültigkeit garantieren. Das spielt insbesondere bei der Entwicklung von .NET-Anwendungen eine wesentliche Rolle, um die Plattformunabhängigkeit des Codes zu gewährleisten. Streams dienen generell dazu, drei elementare Operationen ausführen zu können: Dateninformationen müssen in einen Stream geschrieben werden. Nach welchem Muster das geschieht, wird durch den Typ des Streams vorgegeben. Aus dem Datenstrom muss gelesen werden, ansonsten könnte man die Daten nicht weiterverarbeiten. Das Ziel kann unterschiedlich sein: Die Bytes können Variablen oder Arrays zugewiesen werden, sie könnten aber auch in einer Datenbank landen und zur Ausgabe an einem Peripheriegerät wie dem Drucker oder dem Monitor dienen. Nicht immer ist es erforderlich, den Datenstrom vom ersten bis zum letzten Byte auszuwerten. Manchmal reicht es aus, erst ab einer bestimmten Position zu lesen. Man spricht dann vom wahlfreien Zugriff. Nicht alle Datenströme können diese drei Punkte gleichzeitig erfüllen. Beispielsweise unterstützen Datenströme im Netzwerk nicht den wahlfreien Zugriff. Bei den Streams werden grundsätzlich zwei Typen unterschieden: Base-Streams, die direkt aus einem Strom Daten lesen oder in diesen hineinschreiben. Diese Vorgänge können z. B. in Dateien, im Hauptspeicher oder in einer Netzwerkverbindung enden. Pass-Through-Streams ergänzen einen Base-Stream um spezielle Funktionalitäten. So können manche Streams verschlüsselt oder im Hauptspeicher gepuffert werden. Pass-Through-Streams lassen sich hintereinander in Reihe schalten, um so die Fähigkeiten eines Base-Streams zu erweitern. Auf diese Weise lassen sich sogar individuelle Streams konstruieren. ### 12.3.1 Die abstrakte Klasse »Stream« Die Klasse Stream ist die abstrakte Basisklasse aller anderen Stream-Klassen. Sie stellt alle fundamentalen Eigenschaften und Methoden bereit, die von den abgeleiteten Klassen geerbt werden und letztendlich deren Funktionalität ausmachen. Die von der Klasse Stream abgeleiteten Klassen unterstützen mit ihren Methoden nur Operationen auf Bytesequenzen. Da allein durch eine Bytesequenz noch keine Aussage darüber getroffen ist, welcher Datentyp sich hinter mehreren aufeinanderfolgenden Bytes verbirgt, muss der Inhalt eines solchen Stroms noch interpretiert werden. #### Eigenschaften der Klasse »Stream« Streams stellen Schreib-, Lese- und Suchoperationen bereit. Allerdings unterstützt nicht jeder Stream gleichzeitig alle Operationen. Um in einem gegebenen Stream dessen Verhaltensweisen festzustellen, können Sie die Eigenschaften CanRead, CanWrite und CanSeek abfragen, die einen booleschen Wert zurückliefern und Auskunft über die Charakteristik dieses Stream-Objekts liefern. Die Eigenschaft Length liefert die Länge des Streams, und Position liefert die aktuelle Position innerhalb des Streams. Position wird allerdings nur von den Streams bereitgestellt, die auch die Positionierung mit der Seek-Methode unterstützen.

Tabelle 12.12 Eigenschaften der abstrakten Klasse »Stream«

Eigenschaft | Beschreibung
CanRead | Ruft in einer abgeleiteten Klasse einen Wert ab, der angibt, ob der aktuelle Stream Lesevorgänge unterstützt.
CanWrite | Ruft in einer abgeleiteten Klasse einen Wert ab, der angibt, ob der aktuelle Stream Schreibvorgänge unterstützt.
CanSeek | Ruft in einer abgeleiteten Klasse einen Wert ab, der angibt, ob der aktuelle Stream Suchvorgänge unterstützt.
Length | Ruft in einer abgeleiteten Klasse die Länge des Streams in Byte ab.
Position | Ruft in einer abgeleiteten Klasse die Position im aktuellen Stream ab oder legt diese fest.
#### Methoden der Klasse »Stream« Die wichtigsten Methoden aller Stream-Klassen dürften Read, Write und Seek sein. Sehen wir uns zunächst die Definitionen der beiden Methoden Read und Write an, die von jeder abgeleiteten Klasse überschrieben werden müssen.
    public abstract int Read(in byte[] buffer,int offset,int count);
    public abstract void Write(byte[] buffer, int offset, int count);
Einem schreibenden Stream müssen Sie die Daten übergeben, die in den Datenstrom geschrieben werden sollen. Die Write-Methode benutzt dazu den ersten Parameter, liest die Elemente byteweise ein und schreibt sie in den Strom. Der Empfänger des Datenstroms kann die Bytes mit Read im ersten Parameter entnehmen. Der zweite Parameter offset bestimmt die Position im Array, ab der der Lese- bzw. Schreibvorgang beginnen soll. Meistens wird hier die Zahl 0 eingetragen, das heißt, die Operation greift auf das erste Array-Element zu – entweder lesend oder schreibend. Im dritten und letzten Parameter wird angegeben, wie viele Bytes gelesen oder geschrieben werden sollen. Beachten Sie auch, dass Write als Methode ohne Rückgabewert implementiert ist, während Read einen int liefert, dem Sie die Anzahl der gelesenen Bytes entnehmen können, die in den Puffer – also das Array – geschrieben worden sind. Der Rückgabewert ist 0, wenn das Ende des Streams erreicht ist. Er kann aber auch kleiner sein, als im dritten Parameter angegeben ist, wenn weniger Bytes im Stream eingelesen werden. Die abstrakte Klasse Stream definiert zwei weitere, ähnliche Methoden, die jedoch jeweils nur immer ein Byte aus dem Datenstrom lesen oder in diesen hineinschreiben: ReadByte und WriteByte. Beide Methoden sind parameterlos und setzen den Positionszeiger innerhalb des Streams um eine (Byte-)Position weiter.
    public virtual int ReadByte();
    public virtual void WriteByte(byte value);
Der Rückgabewert der ReadByte-Methode ist –1, wenn das Ende des Datenstroms erreicht ist. Um in einem Datenstrom ab einer vorgegebenen Position zu lesen oder zu schreiben, bietet sich die S eek-Methode an:
    public abstract long Seek(long offset, SeekOrigin origin);
Mit den beiden Parametern offset und origin wird der Startpunkt für den Positionszeiger im Stream festgelegt, ab dem weitere E/A-Operationen aktiv werden. offset beschreibt die Verschiebung in Bytes ab der unter origin festgelegten Ursprungsposition. origin ist vom Typ der Aufzählung SeekOrigin, in der die drei Konstanten aus Tabelle 12.13 definiert sind.

Tabelle 12.13 Konstantenliste der Aufzählung »SeekOrigin«

Member | Beschreibung
Begin | Gibt den Anfang eines Streams an.
Current | Gibt die aktuelle Position innerhalb eines Streams an.
End | Gibt das Ende eines Streams an.
Mit SeekOrigin.Begin wird der Positionszeiger auf das erste Byte des Datenstroms gesetzt, mit SeekOrigin.Current behält er seine augenblickliche Position bei, und mit SeekOrigin.End wird er auf das Byte gesetzt, das als Erstes den Bytes des vollständigen Streams folgt. Ausgehend von origin wird durch Addition von offset die gewünschte Startposition ermittelt. Ein Stream, der einmal geöffnet worden ist und Daten in den Puffer geschrieben hat, sollte ordnungsgemäß mit Close geschlossen werden. Sie haben jetzt so viele Methoden im Schnelldurchlauf kennengelernt, dass alle erwähnten noch einmal in übersichtlicher tabellarischer Form zusammengefasst werden sollen (siehe Tabelle 12.14).

Tabelle 12.14 Methoden der abstrakten Klasse »Stream«

Methode | Beschreibung
Close | Schließt den aktuellen Stream und gibt alle dem aktuellen Stream zugeordneten Ressourcen frei.
Read | Liest eine Folge von Bytes aus dem aktuellen Stream und setzt den Datenzeiger im Stream um die Anzahl der gelesenen Bytes weiter.
ReadByte | Liest ein Byte aus dem Stream und erhöht die Position im Stream um ein Byte. Der Rückgabewert ist –1, wenn das Ende des Streams erreicht ist.
Seek | Legt die Position im aktuellen Stream fest.
Write | Schreibt eine Folge von Bytes in den aktuellen Stream und erhöht den Datenzeiger im Stream um die Anzahl der geschriebenen Bytes.
WriteByte | Schreibt ein Byte an die aktuelle Position im Stream und setzt den Datenzeiger um eine Position im Stream weiter.
### 12.3.2 Die von »Stream« abgeleiteten Klassen im Überblick Sie haben in den vorherigen Ausführungen nur die wichtigsten Methoden und Eigenschaften der Klasse Stream kennengelernt. Die bisherigen Aussagen sollten genügen, um eine Vorstellung davon zu erhalten, welche wesentlichen Verhaltensweisen an die abgeleiteten Klassen weitervererbt werden. Den in Tabelle 12.15 aufgeführten Klassen dient Stream als Basisklasse. Dabei ist die Tabelle nicht vollständig, sondern beinhaltet nur die wichtigsten Typen. Beachten Sie bitte, dass die verschiedenen Subklassen nicht alle demselben Namespace angehören.

Tabelle 12.15 Die von »Stream« abgeleiteten Klassen

Stream-Typ | Beschreibung
BufferedStream | Die Klasse BufferedStream wird benutzt, um Daten eines anderen E/A-Datenstroms zu puffern. Ein Puffer ist ein Block von Bytes im Arbeitsspeicher des Rechners, der dazu benutzt wird, den Datenstrom zu cachen, um damit die Anzahl der Aufrufe an das Betriebssystem zu verringern. Dadurch lässt sich insgesamt die Effizienz verbessern. Diese Klasse wird immer im Zusammenhang mit anderen Klassen eingesetzt.
CryptoStream | Daten, die nicht in ihrem Originalzustand in einen Strom geschrieben werden sollen, lassen sich mit der Klasse CryptoStream verschlüsseln. CryptoStream wird immer mit einem anderen Stream kombiniert.
FileStream | Diese Klasse wird dazu benutzt, um Daten in Dateien des Dateisystems zu schreiben. Eine Netzwerkverbindung kann ebenfalls das Ziel dieses Datenstroms sein.
GZipStream | Mit den Methoden dieser Klasse können Sie Byte-Ströme komprimieren und dekomprimieren.
MemoryStream | Meistens sind Dateien oder Netzwerkverbindungen das Ziel der Datenströme. Es kann jedoch auch sinnvoll sein, Daten bewusst temporär in den Hauptspeicher zu schreiben und sie später von dort wieder zu lesen. Viele Anwendungen arbeiten nach dem Prinzip, Daten in eine temporäre Datei zu speichern. Ein MemoryStream kann temporäre Dateien ersetzen und trägt so dazu bei, die Leistungsfähigkeit einer Anwendung zu steigern, da das Schreiben und Lesen in den Hauptspeicher um ein Vielfaches schneller ist als das Schreiben auf die Festplatte.
NetworkStream | Ein Datenfluss, der auf der Klasse NetworkStream basiert, sendet die Daten basierend auf Sockets. Das Besondere an diesem Datenstrom ist, dass er nur die Fähigkeit hat, Daten vollständig in den Strom zu schreiben oder aus diesem zu lesen. Der Zugriff auf beliebige Daten innerhalb des Stroms ist nicht möglich.
### 12.3.3 Die Klasse »FileStream« Die Klasse FileStream ist die universellste Klasse und erscheint damit in vielen Anwendungsfällen am geeignetsten. Sie hat die Fähigkeit, sowohl byteweise aus einer Datei zu lesen als auch byteweise in eine Datei zu schreiben. Außerdem kann ein Positionszeiger auf eine beliebige Position innerhalb des Streams gesetzt werden. Ein FileStream puffert die Daten, um die Ausführungsgeschwindigkeit zu erhöhen. Die Größe des Puffers beträgt standardmäßig 8 KByte. Die FileStream-Klasse bietet eine Reihe von Konstruktoren an, um dem Objekt bestimmte Verhaltensweisen und Eigenschaften mit auf den Lebensweg zu geben:
    public FileStream(string, FileMode);
    public FileStream(string, FileMode, FileAccess);
    public FileStream(string, FileMode, FileAccess, FileShare);
    public FileStream(string, FileMode, FileAccess, FileShare, int);
    public FileStream(string, FileMode, FileAccess, FileShare, int, bool);
Sie können ein FileStream-Objekt erzeugen, indem Sie im ersten Parameter eine Pfadangabe als Zeichenfolge übergeben. Der Parameter FileMode beschreibt, wie das Betriebssystem die Datei öffnen soll (FileMode.Append, FileMode.Create, FileMode.CreateNew …). FileAccess hingegen gibt an, wie auf die Datei zugegriffen werden darf (FileAccess.Read, FileAccess.Write oder FileAccess.ReadWrite). Sie haben diese Typen bereits im Abschnitt zur Klasse File kennengelernt (siehe auch die Tabellen 12.2 und 12.3). Der Parameter vom Typ FileShare gibt an, ob ein gemeinsamer Zugriff auf die Datei möglich ist oder nicht (siehe auch Tabelle 12.4). Der Puffer, in den ein FileStream die Daten zur Steigerung der Leistungsfähigkeit schreibt, ist standardmäßig 8 KByte groß. Mit dem Parameter vom Typ int können Sie die Größe des Puffers bei der Instanziierung beeinflussen. Mit dem letzten Parameter vom Typ bool kann noch angegeben werden, ob das Objekt asynchrone Zugriffe unterstützen soll. #### Schreiben in einen »FileStream« Das folgende Codefragment demonstriert, wie mit einem FileStream-Objekt Daten in eine Datei geschrieben werden:
    static void Main(string[] args) {
      byte[] arr = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
      string path = @"D:\Testfile.txt";
      FileStream fs = new FileStream(path, FileMode.Create);
      fs.Write(arr, 0, arr.Length);
      fs.Close();
    }
Zunächst wird ein byte-Array deklariert und mit insgesamt zehn Zahlen initialisiert. In der zweiten Anweisung wird der Name der Datei festgelegt, in die das Array geschrieben werden soll. Bei der Instanziierung des FileStream-Objekts werden dem Konstruktor im ersten Argument der Pfad und die Datei bekannt gegeben, auf der der Stream operieren soll. Die Fähigkeiten dieses Streams beschreibt das zweite Argument: Die Konstante FileMode.Create teilt dem Konstruktor mit, dass das FileStream-Objekt eine neue Datei erzeugen kann oder, falls im angegebenen Pfad bereits eine gleichnamige Datei existiert, diese überschreiben soll. Mit
    fs.Write(arr, 0, arr.Length);
wird der Inhalt des Arrays arr dem Stream-Objekt übergeben. Die Syntax der Methode Write der Klasse FileStream lautet wie folgt:
    public void Write(byte[] array, int offset, int count);
Dabei haben die drei Parameter die folgende Bedeutung:

Tabelle 12.16 Die Parameter der Methode »Write« eines »FileStream«-Objekts

Parameter | Beschreibung
array | Byte-Array, in das die übergebenen Daten gelesen werden
offset | Indexposition im Array, an der die Leseoperation beginnen soll
count | Anzahl der zu lesenden Bytes
Der Schreibvorgang des Beispiels startet mit dem ersten Array-Element. Das sagt der zweite Parameter der Write-Methode aus. Die Anzahl der zu schreibenden Bytes bestimmt der dritte Parameter – in unserem Beispiel werden alle Array-Elemente dem Datenstrom zugeführt. Zum Schluss wird der FileStream mit der Methode Close geschlossen. #### Lesen aus einem »FileStream« Wir wollen uns nun auch vom Erfolg unserer Bemühungen überzeugen und die Datei auswerten. Dazu ergänzen wir den Programmcode des Beispiels wie folgt:
    static void Main(string[] args) {
      ...
      byte[] arrRead = new byte[10];
      fs.Read(arrRead, 0, 10);
      for (int i = 0; i < arr.Length; i++)
        Console.WriteLine(arrRead[i]);
      fs.Close();
    }
Wir deklarieren ein weiteres Array (arrRead), in das wir das Ergebnis der Leseoperation hineinschreiben. Da uns bekannt ist, wie viele Byte-Elemente sich in unserer Datei befinden (wie unfair), können wir die Array-Grenze schon im Voraus festlegen. Nun kommt es zum Aufruf der Read-Methode. Zuerst wollen wir uns wieder die Syntax dieser Methode anschauen:
    public override int Read(in byte[] array, int offset, int count);
Die Parameter sind denen der Write-Methode sehr ähnlich. Das FileStream-Objekt, auf dem die Read-Methode aufgerufen wird, repräsentiert eine bestimmte Datei. Diese wurde bereits über den Konstruktor bekannt gegeben. Aus der Datei werden die Daten in das Array eingelesen, das durch den Parameter array beschrieben wird. Der erste Array-Index, der der Schreiboperation zur Verfügung steht, wird im Parameter offset angegeben, und die Anzahl der aus dem FileStream zu lesenden Bytes steht im dritten Parameter (count). Wir wollen den ersten Byte-Wert aus dem Datenstrom in das mit 0 indizierte Element des Arrays arrRead schreiben und geben das im zweiten Parameter bekannt. Die Gesamtanzahl der zu lesenden Bytes teilen wir dem dritten Parameter mit. In einer Schleife wird danach das eingelesene Array durchlaufen und an der Konsole ausgegeben. Starten Sie nun die Laufzeit des Programms, wird die Enttäuschung groß sein und Sie an den eigenen Programmierfähigkeiten zweifeln lassen! Denn bedauerlicherweise werden nicht die Zahlenwerte ausgegeben, die wir in die Datei geschrieben haben, sondern nur Nullen. Haben wir etwas falsch gemacht, und wenn ja, wie ist das zu erklären? #### Positionszeiger Die Schreib- bzw. Leseposition in einem Datenstrom wird durch einen Positionszeiger beschrieben. Schließlich muss der Strom wissen, auf welchem Byte die folgende Operation ausgeführt werden soll. Bei der Instanziierung eines Stream-Objekts verweist der Zeiger zunächst auf das erste Byte im Stream. Mit dem Aufruf der Write-Methode wird ein Wert daher genau an diese Position geschrieben. Anschließend wird der Positionszeiger auf die folgende Byte-Position verschoben. Dieser Vorgang wiederholt sich bei jedem Schreibvorgang, von denen es in unserem Beispiel zehn gibt, nämlich für jedes zu schreibende Array-Element einen. Am Ende, wenn wir unsere zehn Bytes in den Strom geschrieben haben, verweist der Positionszeiger auf das folgende, nun elfte Byte im Stream (siehe Abbildung 12.3). Genau das verursacht nun ein Problem. Wir rufen auf dem FileStream-Objekt die Read-Methode auf und lesen ab der Position, die aktuell durch den Datenzeiger beschrieben wird. Das ist aber die elfte Stelle im Datenstrom – und nicht die erste, wie wir es eigentlich erwartet haben bzw. wie es hätte sein sollen. Hier klicken, um das Bild zu vergrößern Abbildung 12.3 Der Positionszeiger in einem »Stream«-Objekt Kommen wir nun zur Lösung. FileStream beerbt die Klasse Stream und hat daher auch eine Seek-Methode, mit der wir den Positionszeiger beliebig einstellen können:
    public override long Seek(long offset, SeekOrigin origin);
Wir überlegen uns, wohin wir den Ursprung des Positionszeigers verlegen wollen – natürlich an den Anfang des Datenstroms. Also muss der zweite Parameter der Seek-Methode auf
    origin = SeekOrigin.Begin
festgelegt werden (siehe dazu auch Tabelle 12.13). Nun geben wir im ersten Argument den tatsächlichen und endgültigen Startpunkt des Positionszeigers bezogen auf den im zweiten Parameter definierten Ursprung an. Er lautet 0, denn schließlich wollen wir den Zeiger auf die erste Position des Datenstroms setzen.
    ...
    fs.Write(arr, 0, arr.Length);
    ...
    fs.Seek(0, SeekOrigin.Begin);
    fs.Read(arrRead, 0, 10);
    ...
Natürlich wäre es auch möglich, zunächst das FileStream-Objekt zu schließen und es danach neu zu instanziieren. Damit hätten wir wieder einen Positionszeiger, der auf das erste Byte im Stream zeigt. Wenn wir nach der Ergänzung mit Seek das Programm noch einmal starten, wird das Ergebnis wie erwartet ausgegeben. Zum Schluss wollen wir noch einmal den gesamten Code zusammenfassen.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 12\FileStreamSample
    // ---------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        byte[] arr = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
        string path = "D:\\Testfile.txt";
        // FileStream öffnen
        FileStream fs = new FileStream(path, FileMode.Create);
        // in den Stream schreiben
        fs.Write(arr, 0, arr.Length);
        byte[] arrRead = new byte[10];
        // Positionszeiger auf den Anfang des Streams setzen
        fs.Seek(0, SeekOrigin.Begin);
        // Stream lesen
        fs.Read(arrRead, 0, 10);
        for (int i = 0; i < arr.Length; i++)
          Console.WriteLine(arrRead[i]);
        Console.ReadLine();
        // FileStream schließen
        fs.Close();
      }
    }
Wir müssen nicht zwangsläufig das komplette Byte-Array vom ersten bis zum letzten Element in die Datei schreiben. Mit
    fs.Write(arr, 2, arr.Length - 2);
ist das erste zu schreibende Element das, das die Zahl 30 enthält (entsprechend dem Index 2). Wenn wir allerdings den dritten Parameter, der die Anzahl der zu lesenden Bytes angibt, nicht entsprechend anpassen, wird über das Ende des Arrays hinaus gelesen, was zu der Exception ArgumentException führt. #### Textdatei mit »FileStream« lesen Obwohl es spezialisierte Klassen zum Lesen und Schreiben in eine Textdatei gibt, lässt sich das auch mit einem FileStream-Objekt realisieren. Wir wollen uns das im Folgenden ansehen:
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 12\TextdateiMitFileStream
    // ---------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        // Benutzereingabe anfordern
        Console.Write("Geben Sie die zu öffnende Datei an: ");;
        string strFile = Console.ReadLine();
        // prüfen, ob die angegebene Datei existiert
        if (! File.Exists(strFile)) {
          Console.WriteLine("Die Datei {0} existiert nicht!", strFile);
          Console.ReadLine();
          return;
        }
        // Datei öffnen
        FileStream fs = File.Open(strFile, FileMode.Open);
        // Byte-Array, in das die Daten aus dem Datenstrom eingelesen werden
        byte[] puffer = new Byte[fs.Length];
        // Zeichen aus der Datei lesen und in das Array
        // schreiben, der Lesevorgang beginnt mit dem ersten Zeichen
        fs.Read(puffer, 0, (int)fs.Length);
        // Byte-Array elementweise einlesen und jedes
        // Array-Element in Char konvertieren
        for (int i = 0; i < fs.Length; i++)
          Console.Write(Convert.ToChar(puffer[i]));
        Console.ReadLine();
      }
    }
Nach dem Start der Laufzeitumgebung wird der Benutzer dazu aufgefordert, den Pfad zu einer Textdatei anzugeben. Diesmal beschreiten wir allerdings einen anderen Weg und rufen den Konstruktor nicht direkt auf, sondern die Methode Open der Klasse File:
    FileStream fs = File.Open(strFile, FileMode.Open);
Diese Anweisung funktioniert tadellos, weil der Rückgabewert der File.Open-Methode die Referenz auf eine FileStream-Instanz liefert. Gegen den Weg über einen FileStream-Konstruktor, der diese Möglichkeit auch bietet, ist zwar grundsätzlich nichts einzuwenden, wir wollen uns allerdings die gezeigte Alternative vor Augen führen. Im folgenden Schritt deklarieren wir das Byte-Array puffer und initialisieren es mit einer Kapazität, die der Größe der Datei entspricht:
    byte[] puffer = new Byte[fs.Length];
Die Größe der Datei besorgen wir uns mit der Eigenschaft Length der Klasse FileStream, die uns die Größe des Datenstroms liefert. Daran schließt sich die Leseoperation mit Read an, die den Inhalt der Textdatei byteweise liest und in das Array puffer schreibt:
    fs.Read(puffer, 0, (int)fs.Length);
Weil der dritte Parameter der Read-Methode ein Datum vom Typ int erwartet und die Eigenschaft Length einen long liefert, müssen wir noch in den richtigen Datentyp konvertieren. Ein FileStream-Objekt arbeitet grundsätzlich nur auf der Basis von Bytes, es weiß nichts von dem tatsächlichen Typ, der sich im Datenstrom verbirgt. Eine Textdatei enthält aber Zeichen, die, als ANSI-Zeichen interpretiert, erst den wirklichen Informationsgehalt liefern. Deshalb müssen wir jedes einzelne Byte des Streams in einen char-Typ konvertieren. Das geschieht in einer Schleife, die alle Bytes des Arrays abgreift, konvertiert und danach an der Konsole ausgibt.
    for (int i = 0; i < fs.Length; i++)
      Console.Write(Convert.ToChar(puffer[i]));