21.2 Reading and Writing Data

Reading and writing data is accomplished with the Stream class. Remember streams? This is a chapter about streams.[1]

[1] With a tip of the hat to Arlo Guthrie.

Stream supports synchronous and asynchronous reads and writes. The .NET Framework provides a number of classes derived from Stream, including FileStream, MemoryStream, and NetworkStream. In addition, there is a BufferedStream class, which provides buffered I/O and which can be used in conjunction with any of the other stream classes. The principal classes involved with I/O are summarized in Table 21-5.

Table 21-5. Principle I/O classes of the .NET Framework

Class

Use

Stream

Abstract class that supports reading and writing bytes.

BinaryReader/BinaryWriter

Read and write encoded strings and primitive datatypes to and from streams.

File, FileInfo, Directory, DirectoryInfo

Provide implementations for the abstract FileSystemInfo classes, including creating, moving, renaming, and deleting files and directories.

FileStream

For reading to and from File objects; supports random access to files. Opens files synchronously by default; supports asynchronous file access.

TextReader,TextWriter, StringReader,StringWriter

TextReader and TextWriter are abstract classes designed for Unicode character I/O. StringReader and StringWriter write to and from strings, allowing your input and output to be either a stream or a string.

BufferedStream

A stream that adds buffering to another stream such as a NetworkStream. Note that FileStream has buffering built in. BufferedStreams can improve performance of the stream to which they are attached.

MemoryStream

A nonbuffered stream whose encapsulated data is directly accessible in memory. A MemoryStream has no backing store, and is most useful as a temporary buffer.

NetworkStream

A stream over a network connection.

21.2.1 Binary Files

This section starts by using the basic Stream class to perform a binary read of a file. The term binary read is used to distinguish from a text read. If you don't know for certain that a file is just text, it is safest to treat it as a stream of bytes, known as a binary file.

The Stream class is chock-a-block with methods, but the most important are Read( ), Write( ), BeginRead( ), BeginWrite( ), and Flush( ). All of these are covered in the next few sections.

To perform a binary read, begin by creating a pair of Stream objects, one for reading and one for writing:

Stream inputStream = File.OpenRead(
    @"C:\test\source\test1.cs");

Stream outputStream = File.OpenWrite(
    @"C:\test\source\test1.bak");

To open the files to read and write, use the static OpenRead( ) and OpenWrite( ) methods of the File class. The static overload of these methods takes the path for the file as an argument, as shown previously.

Binary reads work by reading into a buffer. A buffer is just an array of bytes that will hold the data read by the Read( ) method.

Pass in the buffer, the offset in the buffer at which to begin storing the data read in, and the number of bytes to read. InputStream.Read reads bytes from the backing store into the buffer and returns the total number of bytes read.

It continues reading until no more bytes remain.

while ( (bytesRead = 
   inputStream.Read(buffer,0,SIZE_BUFF)) > 0 )
{
    outputStream.Write(buffer,0,bytesRead);
}

Each buffer-ful of bytes is written to the output file. The arguments to Write( ) are the buffer from which to read, the offset into that buffer at which to start reading, and the number of bytes to write. Notice that you write the same number of bytes as you just read.

Example 21-4 provides the complete listing.

Example 21-4. Implementing a binary read and write to a file
namespace Programming_CSharp
{
   using System;
   using System.IO;

   class Tester
   {
      const int SizeBuff = 1024;

      public static void Main( )
      {
         // make an instance and run it
         Tester t = new Tester( );
         t.Run( );
      }
        
      // Set it running with a directory name
      private void Run( )
      {
         // the file to read from
         Stream inputStream = File.OpenRead(
            @"C:\test\source\test1.cs");

         // the file to write to
         Stream outputStream = File.OpenWrite(
            @"C:\test\source\test1.bak");

         // create a buffer to hold the bytes 
         byte[] buffer = new Byte[SizeBuff];
         int bytesRead; 

         // while the read method returns bytes
         // keep writing them to the output stream
         while ( (bytesRead = 
            inputStream.Read(buffer,0,SizeBuff)) > 0 )
         {
            outputStream.Write(buffer,0,bytesRead);
         }

         // tidy up before exiting
         inputStream.Close( );
         outputStream.Close( );
      }
   }
}

The result of running this program is that a copy of the input file (test1.cs) is made in the same directory and named test1.bak.

21.2.2 Buffered Streams

In the previous example, you created a buffer to read into. When you called Read( ), a buffer-ful was read from disk. It might be, however, that the operating system can be much more efficient if it reads a larger (or smaller) number of bytes at once.

A buffered stream object allows the operating system to create its own internal buffer, and read bytes to and from the backing store in whatever increments it thinks is most efficient. It will still fill your buffer in the increments you dictate, but your buffer is filled from the in-memory buffer, not from the backing store. The net effect is that the input and output are more efficient and thus faster.

A BufferedStream object is composed around an existing Stream object that you already have created. To use a BufferedStream, start by creating a normal stream class as you did in Example 21-4:

Stream inputStream = File.OpenRead(
    @"C:\test\source\folder3.cs");

Stream outputStream = File.OpenWrite(
    @"C:\test\source\folder3.bak");

Once you have the normal stream, pass that stream object to the buffered stream's constructor:

BufferedStream bufferedInput = 
    new BufferedStream(inputStream);

BufferedStream bufferedOutput = 
    new BufferedStream(outputStream);

You can then use the BufferedStream as a normal stream, calling Read( ) and Write( ) just as you did before. The operating system handles the buffering:

while ( (bytesRead = 
     bufferedInput.Read(buffer,0,SIZE_BUFF)) > 0 )
 {
     bufferedOutput.Write(buffer,0,bytesRead);
 }

The only change is that you must remember to flush the buffer when you want to ensure that the data is written out to the file:

bufferedOutput.Flush( );

This essentially tells the operating system to take the entire contents of the in-memory buffer and write it out to disk.

Example 21-5 provides the complete listing.

Example 21-5. Implementing buffered I/O
namespace Programming_CSharp
{
   using System;
   using System.IO;

   class Tester
   {
      const int SizeBuff = 1024;

      public static void Main( )
      {
         // make an instance and run it
         Tester t = new Tester( );
         t.Run( );
      }
        
      // Set it running with a directory name
      private void Run( )
      {
         // create binary streams
         Stream inputStream = File.OpenRead(
            @"C:\test\source\folder3.cs");

         Stream outputStream = File.OpenWrite(
            @"C:\test\source\folder3.bak");

         // add buffered streams on top of the
         // binary streams
         BufferedStream bufferedInput = 
            new BufferedStream(inputStream);

         BufferedStream bufferedOutput = 
            new BufferedStream(outputStream);
         byte[] buffer = new Byte[SizeBuff];
         int bytesRead;

         while ( (bytesRead = 
            bufferedInput.Read(buffer,0,SizeBuff)) > 0 )
         {
            bufferedOutput.Write(buffer,0,bytesRead);
         }

         bufferedOutput.Flush( );
         bufferedInput.Close( );
         bufferedOutput.Close( );

      }
   }
}

With larger files, this example should run more quickly than Example 21-4 did.

21.2.3 Working with Text Files

If you know that the file you are reading (and writing) contains nothing but text, you might want to use the StreamReader and StreamWriter classes. These classes are designed to make manipulation of text easier. For example, they support the ReadLine( ) and WriteLine( ) methods that read and write a line of text at a time. You've used WriteLine( ) with the Console object.

To create a StreamReader instance, start by creating a FileInfo object and then call the OpenText( ) method on that object:

FileInfo theSourceFile = 
   new FileInfo (@"C:\test\source\test1.cs");

StreamReader stream = theSourceFile.OpenText( );

OpenText( ) returns a StreamReader for the file. With the StreamReader in hand, you can now read the file, line by line:

do
{
    text = stream.ReadLine( );
} while (text != null); 

ReadLine( ) reads a line at a time until it reaches the end of the file. The StreamReader will return null at the end of the file.

To create the StreamWriter class, call the StreamWriter constructor, passing in the full name of the file you want to write to:

StreamWriter writer = new
StreamWriter(@"C:\test\source\folder3.bak",false);

The second parameter is the Boolean argument append. If the file already exists, true will cause the new data to be appended to the end of the file, and false will cause the file to be overwritten. In this case, pass in false, overwriting the file if it exists.

You can now create a loop to write out the contents of each line of the old file into the new file, and while you're at it, to print the line to the console as well:

do
{
    text = reader.ReadLine( );
    writer.WriteLine(text);
    Console.WriteLine(text);
} while (text != null); 

Example 21-6 provides the complete source code.

Example 21-6. Reading and writing to a text file
namespace Programming_CSharp
{
   using System;
   using System.IO;

   class Tester
   {
      public static void Main( )
      {
         // make an instance and run it
         Tester t = new Tester( );
         t.Run( );
      }
        
      // Set it running with a directory name
      private void Run( )
      {
         // open a file
         FileInfo theSourceFile = new FileInfo(
            @"C:\test\source\test.cs");

         // create a text reader for that file
         StreamReader reader = theSourceFile.OpenText( );

         // create a text writer to the new file
         StreamWriter writer = new StreamWriter(
            @"C:\test\source\test.bak",false);

         // create a text variable to hold each line
         string text;

         // walk the file and read every line
         // writing both to the console
         // and to the file
         do
         {
            text = reader.ReadLine( );
            writer.WriteLine(text);
            Console.WriteLine(text);
         } while (text != null); 

         // tidy up
         reader.Close( );
         writer.Close( );
      }
   }
}

When this program is run, the contents of the original file are written both to the screen and to the new file. Notice the syntax for writing to the console:

Console.WriteLine(text);

This syntax is nearly identical to that used to write to the file:

writer.WriteLine(text);

The key difference is that the WriteLine( ) method of Console is static, while the WriteLine( ) method of StreamWriter, which is inherited from TextWriter, is an instance method, and thus must be called on an object rather than on the class itself.



    Part I: The C# Language