Chapter 3. Fishing the FileStream

In This Chapter

  • Reading and writing data files

  • Using the Stream classes

  • Using the using statement

  • Dealing with input/output errors

I once caught two trout on a single hook, in a lovely mountain stream in my native Colorado — quite a thrill for an 11-year-old. Fishing the "file stream" with C# isn't quite so thrilling, but it's one of those indispensable programming skills.

File access refers to the storage and retrieval of data on the disk. I cover basic text-file input/output in this chapter. Reading and writing data from databases is covered in Chapter 2 of this minibook, and reading and writing information to the Internet is covered in Chapter 4.

Going Where the Fish Are: The File Stream

The console application programs in this book mostly take their input from, and send their output to, the console. Programs outside this chapter have better — or at least different — things to bore you with than file manipulation. I don't want to confuse their message with the extra baggage of involved input/output (I/O). However, console applications that don't perform file I/O are about as common as Sierra Club banners at a paper mill.

The I/O classes are defined in the System.IO namespace. The basic file I/O class is FileStream. In days past, the programmer would open a file. The open command would prepare the file and return a handle. Usually, this handle was nothing more than a number, like the one they give you when you place an order at a Burger Whop. Every time you wanted to read from or write to the file, you presented this ID.

Streams

C# uses a more intuitive approach, associating each file with an object of class FileStream. The constructor for FileStream opens the file and manages the underlying handle. The methods of FileStream perform the file I/O.

Tip

FileStream isn't the only class that can perform file I/O. However, it represents your good ol' basic file that covers 90 percent of your file I/O needs. This primary class is the one described in this chapter. If it's good enough for C#, it's good enough for me.

The stream concept is fundamental to C# I/O. Think of a parade, which "streams" by you, first the clowns, and then the floats, and then a band or two, some horses, a troupe of Customer objects, a BankAccount, and so on. Viewing a file as a stream of bytes (or characters or strings) is much like a parade. You "stream" the data in and out of your program.

The .NET classes used in C# include an abstract Stream base class and several subclasses, for working with files on the disk, over a network, or already sitting as chunks of data in memory. Some stream classes specialize in encrypting and decrypting data, some are provided to help speed up I/O operations that might be slow using one of the other streams, and you're free to extend class Stream with your own subclass if you come up with a great idea for a new stream (although I warn you that extending Stream is arduous). I give you a tour of the stream classes in the later section "Exploring More Streams than Lewis and Clark."

Readers and writers

FileStream, the stream class you'll probably use the most, is a basic class. Open a file, close a file, read a block of bytes, and write a block — that's about all you have. But reading and writing files down at the byte level is a lot of work, something I eschew studiously. Fortunately, the .NET class library introduces the notion of "readers" and "writers." Objects of these types greatly simplify file (and other) I/O.

When you create a new reader (of one of several available types), you associate a stream object with it. It's immaterial to the reader whether the stream connects to a file, a block of memory, a network location, or the Mississippi. The reader requests input from the stream, which gets it from — well, wherever. Using writers is quite similar, except that you're sending output to the stream rather than asking for input. The stream sends it to a specified destination. Often that's a file, but not always.

The System.IO namespace contains classes that wrap around FileStream (or other streams) to give you easier access and that warm fuzzy feeling:

  • TextReader/TextWriter: A pair of abstract classes for reading characters (text). These classes are the base for two flavors of subclasses: StringReader/StringWriter and StreamReader/StreamWriter.

Note

Because TextReader and TextWriter are abstract, you'll use one of their subclass pairs, usually StreamReader/StreamWriter, to do actual work. I explain abstract classes in Book II.

  • StreamReader/StreamWriter: A more sophisticated text reader and writer for the more discriminating palate — not to mention that they aren't abstract, so you can even read and write with them. For example, StreamWriter has a WriteLine() method much like that in the Console class. StreamReader has a corresponding ReadLine() method and a handy ReadToEnd() method that grabs the whole text file in one gulp, returning the characters read as a string — which you could then use with a StringReader (discussed later), a foreach loop, the String.Split() method, and so on. Check out the various constructors for these classes in Help.

    You see StreamReader and StreamWriter in action in the next two sections.

One nice thing about reader/writer classes such as StreamReader and StreamWriter is that you can use them with any kind of stream. This makes reading from and writing to a MemoryStream no harder than reading from and writing to the kind of FileStream discussed in earlier sections of this chapter. (I cover MemoryStream later in the chapter.)

See the later section "More Readers and writers" for additional reader/writer pairs.

The following sections provide the FileWrite and FileRead programs, which demonstrate ways to use these classes for text I/O the C# way.

Note

StreamWriting for Old Walter

In the movie On Golden Pond, Henry Fonda spent his retirement years trying to catch a monster trout that he named Old Walter. You aren't out to drag in the big fish, but you should at least cast a line into the stream. This section covers writing to files.

Programs generate two kinds of output:

  • Some programs write blocks of data as bytes in pure binary format. This type of output is useful for storing objects in an efficient way — for example, a file of Student objects that you need to persist (keep on disk in a permanent file).

    See the later section "More Readers and Writers" for the BinaryReader and BinaryWriter classes.

    Note

    A sophisticated example of binary I/O is the persistence of groups of objects that refer to each other (using the HAS_A relationship). Writing an object to disk involves writing identifying information (so its type can be reconstructed when you read the object back in), and then each of its data members, some of which may be references to connected objects, each with its own identifying information and data members. Persisting objects this way is called serialization. You can look it up in Help when you're ready; I don't cover it here. Sophistication is out of my league.

  • Most programs read and write human-readable text: you know, letters, numbers, and punctuation, like Notepad. The human-friendly StreamWriter and StreamReader classes are the most flexible ways to work with the stream classes. For some details, see the earlier section "Readers and writers."

    Note

    Human-readable data was formerly known as ASCII or, slightly later, ANSI, text. These two monikers refer to the standards organization that defined them. However, ANSI encoding doesn't provide the alphabets east of Austria and west of Hawaii; it can handle only Roman letters, like those used in English. It has no characters for Russian, Hebrew, Arabic, Hindi, or any other language using a non-Roman alphabet, including Asian languages such as Chinese, Japanese, and Korean. The modern, more flexible Unicode file format is "backward-compatible" — including the familiar ANSI characters at the beginning of its character set, but still providing a large number of other alphabets, including everything you need for all the languages I just listed. Unicode comes in several variations, called encodings; however, UTF8 is the default format for C#. (You can find out more about encodings and how to use them in the article "Converting between Byte and Char Arrays" at csharp102.info.)

Using the stream: An example

The following FileWrite program reads lines of data from the console and writes them to a file of the user's choosing. This is pseudocode — it isn't meant to compile. I used it only as an example.

Note

// FileWrite -- Write input from the Console into a text file.
using System;
using System.IO;

namespace FileWrite
{
  public class Program
  {
    public static void Main(string[] args)
    {
      // Get a filename from the user -- the while loop lets you
      // keep trying with different filenames until you succeed.
      StreamWriter sw = null;
      string fileName = "";
      while(true)
      {
        try
        {
          // Enter output filename (simply hit Enter to quit).
          Console.Write("Enter filename "
                      + "(Enter blank filename to quit):");
          fileName = Console.ReadLine();
          if (fileName.Length == 0)
          {
            // No filename -- this jumps beyond the while
            // loop to safety. You're done.
            break;
          }

          // I factored out these tasks to simplify the loops a bit.

          // Call a method (below) to set up the StreamWriter.
          sw = PrepareTheStreamWriter(fileName);
          // Read one string at a time, outputting each to the
          // FileStream open for writing.
          ReadAndWriteLines(sw);

          // Done writing, so close the file you just created.
          sw.Close(); // A very important step. Closes the file too.
          sw = null;  // Give it to the garbage collector.
        }
        catch (IOException ioErr)
        {
          // Ooops -- Error occurred during the processing of the
          // file -- tell the user the full name of the file:
          // Tack the name of the default directory to the filename.
          string dir = Directory.GetCurrentDirectory();  // Directory class
          string path = Path.Combine(dir, fileName); // System.IO.Path class
          Console.WriteLine("Error on file {0}", path);

          // Now output the error message in the exception.
          Console.WriteLine(ioErr.Message);
        }
      }
//Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }

    // GetWriterForFile -- Create a StreamWriter set up to write
    //    to the specified file.
    private static StreamWriter GetWriterForFile(string fileName)
    {
      StreamWriter sw;
      // Open file for writing in one of these modes:
      //   FileMode.CreateNew to create a file if it
      //      doesn't already exist or throw an
      //      exception if file exists.
      //   FileMode.Append to append to an existing file
      //      or create a new file if it doesn't exist.
      //   FileMode.Create to create a new file or
      //      truncate an existing file.

      //   FileAccess possibilities are:
      //      FileAccess.Read,
      //      FileAccess.Write,
      //      FileAccess.ReadWrite.
      FileStream fs = File.Open(fileName,
                                FileMode.CreateNew,
                                FileAccess.Write);

      // Generate a file stream with UTF8 characters.
      // Second parameter defaults to UTF8, so can be omitted.
      sw = new StreamWriter(fs, System.Text.Encoding.UTF8);
      return sw;
    }

    // WriteFileFromConsole -- Read lines of text from the console
    //    and spit them back out to the file.
    private static void WriteFileFromConsole(StreamWriter sw)
    {
      Console.WriteLine("Enter text; enter blank line to stop");
      while (true)
      {
        // Read next line from Console; quit if line is blank.
        string input = Console.ReadLine();
        if (input.Length == 0)
        {
          break;
        }
        // Write the line just read to output file.
        sw.WriteLine(input);
        // Loop back up to get another line and write it.
      }
    }
  }
}

FileWrite uses the System.IO namespace as well as System. System.IO contains the file I/O classes.

Revving up a new outboard StreamWriter

The FileWrite program starts in Main() with a while loop containing a try block. This is common for a file-manipulation program.

Note

Encase all file I/O activity in a try block. File I/O can be prone to errors, such as missing files or directories, bad paths, and so on. See Book I for more on exception handling.

The while loop serves two functions:

  • It allows the program to go back and retry in the event of an I/O failure. For example, if the program can't find a file that the user wants to read, the program can ask for the filename again before blowing off the user.

  • Executing a break command from within the program breezes you right past the try block and dumps you off at the end of the loop. This is a convenient mechanism for exiting a method or program. Keep in mind that break only gets you out of the loop it's called in. (Chapter 4 covers loops and break.)

The FileWrite program reads the name of the file to create from the console. The program terminates by breaking out of the while loop if the user enters an empty filename. The key to the program occurs in the call to a GetWriterForFile() method; you can find the method below Main(). The key lines in GetWriterForFile() are

FileStream fs = File.Open(fileName, FileMode.CreateNew, FileAccess.Write);
// ...
sw = new StreamWriter(fs, System.Text.Encoding.UTF8);

In the first line, the program creates a FileStream object that represents the output file on the disk. The FileStream constructor used here takes three arguments:

  • The filename: This is clearly the name of the file to open. A simple name like filename.txt is assumed to be in the current directory (for FileWrite, working inside Visual Studio, that's the inDebug subdirectory of the project directory; it's the directory containing the .EXE file after you build the program). A filename that starts with a backslash, like some directoryfilename.txt, is assumed to be the full path on the local machine. Filenames that start with two slashes — for example, \your machinesome directoryfilename.txt — are resident on other machines on your network. The filename encoding gets rapidly more complicated from here and is beyond the scope of this minibook.

  • The file mode: This argument specifies what you want to do to the file. The basic write modes are create (CreateNew), append (Append), and overwrite (Create). CreateNew creates a new file but throws an IOException if the file already exists. Create mode creates the file if it doesn't exist but overwrites ("truncates") the file if it exists. Just like it sounds, Append adds to the end of an existing file or creates the file if it doesn't exist.

  • The access type: A file can be opened for reading, writing, or both.

Tip

FileStream has numerous constructors, each of which defaults one or both of the mode and access arguments. However, in my humble opinion, you should specify these arguments explicitly because they have a strong effect on the program's clarity. That's good advice in general. Defaults can be convenient for the programmer but confusing for anyone reading the code.

In the second noncomment line of the GetWriterForFile() method, the program "wraps" the newly opened FileStream object in a StreamWriter object, sw. The StreamWriter class wraps around the FileStream object to provide a set of text-friendly methods. This StreamWriter is what the method returns.

The first argument to the StreamWriter constructor is the FileStream object. There's the wrapping. The second argument specifies the encoding to use. The default encoding is UTF8.

Note

You don't need to specify the encoding when reading a file. StreamWriter writes out the encoding type in the first three bytes of the file. The StreamReader reads these three bytes when the file is opened to determine the encoding. Hiding this kind of detail is an advantage that good software libraries provide.

Finally, we're writing!

After setting up its StreamWriter, the FileWrite program begins reading lines of string input from the console (this code is in the WriteFileFromConsole() method, called from Main()). The program quits reading when the user enters a blank line; until then, it gobbles up whatever it's given and spits it into the StreamWriter sw using that class's WriteLine() method.

Note

The similarity between StreamWriter.WriteLine() and Console.WriteLine() is more than a coincidence.

Finally, the stream is closed with the sw.Close() expression. This is important to do, because it also closes the file. (I have more to say about closing things in the next section.)

Note

Tip

Notice that the program nulls the sw reference after closing StreamWriter. A file object is useless after the file has been closed. It is good programming practice to null a reference after it becomes invalid so that you won't try to use it again. (If you do, your code will throw an exception, letting you know about it!) Closing the file and nulling the reference lets the garbage collector claim it (see Book II to meet the friendly collector on your route) and leaves the file available for other programs to open.

The catch block is like a soccer goalie: It's there to catch any file error that may have occurred in the program. The catch outputs an error message, including the name of the errant file. But it doesn't output just a simple filename — it outputs the entire filename, including the path, for your reading pleasure. It does this by using the Path.Combine() method to tack the current directory name, obtained through the Directory class, onto the front of the filename you entered. (Path is a class designed to manipulate path information. Directory provides properties and methods for working with directories.) Book I gives you the goods on exceptions, including the exceptions to exceptions, the exceptions to those — I give up.

Note

The path is the full name of the file folder. For example, in the filename c:user emp directory ext.txt, the path is c:user emp directory.

Note

The Combine() method is smart enough to realize that for a file like c: est.txt, the path isn't in the current directory. Path.Combine() is also the safest way to ensure that the two path segments being combined will combine correctly, including a path separator character between them. (In Windows, the path separator character is . You can obtain the correct separator for whatever operating system your code is running on, whether it's Windows or some brand of Unix, say, with Path.DirectorySeparatorChar. The .NET Framework library is full of features like that, clearly aimed at writing C# programs that run on multiple operating systems, such as Mono for Linux and Unix, which I discuss in this book's Introduction.)

Upon encountering the end of the while loop, either by completing the try block or by being vectored through the catch, the program returns to the top of the while loop to allow the user to write to another file.

A few sample runs of the program appear as follows. My input is boldfaced:

Enter filename (Enter blank filename to quit):TestFile1.txt
Enter text; enter blank line to stop
This is some stuff
So is this
As is this

Enter filename (Enter blank filename to quit):TestFile1.txt
Error on file C:C#ProgramsFileWriteinDebugTestFile1.txt
The file 'C:C#ProgramsFileWriteinDebugTestFile1.txt' already exists.

Enter filename (Enter blank filename to quit):TestFile2.txt
Enter text; enter blank line to stop
I messed up back there. I should have called it
TestFile2.

Enter filename (Enter blank filename to quit):
Press Enter to terminate...

Everything goes smoothly when I enter some random text into TestFile1.txt. When I try to open TestFile1.txt again, however, the program spits out a message, the gist of which is The file already exists, with the filename attached. The path to the file is tortured because the "current directory" is the directory in which Visual Studio put the executable file. Correcting my mistake, I enter an acceptable filename — such as TestFile2.txt — without complaint.

Using some better fishing gear: The using statement

Now that you've seen FileStream and StreamWriter in action, I should point out the more usual way to do stream writing in C# — inside a using statement:

using(<someresource>)
{
  // Use the resource.
}

The using statement is a construct that automates the process of cleaning up after using a stream. On encountering the closing curly brace of the using block, C# manages "flushing" the stream and closing it for you. (To flush a stream is to push any last bytes left over in the stream's buffer out to the associated file before it gets closed. Think of pushing a handle to drain the last water out of your . . . trout stream.) Using using eliminates the common error of forgetting to flush and close a file after writing to it. Don't leave open files lying around.

Without using, you'd need to write:

Stream fileStream = null;
TextWriter writer = null;
try
{
  // Create and use the stream, then ...
}
finally
{
  stream.Flush();
  stream.Close();
  stream = null;
}

Note how I declared the stream and writer above the try block (so they're visible throughout the method). I also declared the fileStream and writer variables using abstract base classes rather than the concrete types FileStream and StreamWriter. That's a good practice. I set them to null so the compiler won't complain about uninitialized variables.

The preferred way to write the key I/O code in the FileWrite example looks more like this:

// Prepare the file stream.
FileStream fs = File.Open(fileName,
                           FileMode.CreateNew,
                           FileAccess.Write);
// Pass the fs variable to the StreamWriter constructor in the using statement.
using (StreamWriter sw = new StreamWriter(fs))
{
// sw exists only within the using block, which is a local scope.

  // Read one string at a time from the console, outputting each to the
  // FileStream open for writing.
  Console.WriteLine("Enter text; enter blank line to stop");
  while (true)
  {
    // Read next line from Console; quit if line is blank.
    string input = Console.ReadLine();
    if (input.Length == 0)
    {
      break;
    }
    // Write the line just read to output file via the stream.
    sw.WriteLine(input);
    // Loop back up to get another line and write it.
  }
}  // sw goes away here, and fs is now closed. So ...
fs = null;  // Make sure you can't try to access fs again.

The items in parentheses after the using keyword are its "resource acquisition" section, where you allocate one or more resources such as streams, readers/writers, fonts, and so on. (If you allocate more than one resource, they have to be of the same type.) Following that section is the enclosing block, bounded by the outer curly braces.

Note

The using statement's block is not a loop. The block only defines a local scope, like the try block or a method's block. (Variables defined within the block, including its head, don't exist outside the block. Thus the StreamWriter sw isn't visible outside the using block.) I discuss scope in Book I.

At the top of the preceding example, in the resource-acquisition section, you set up a resource — in this case, create a new StreamWriter wrapped around the already-existing FileStream. Inside the block is where you carry out all your I/O code for the file.

At the end of the using block, C# automatically flushes the StreamWriter, closes it, and closes the FileStream, also flushing any bytes it still contains to the file on disk. Ending the using block also disposes the StreamWriter object — see the warning and the technical discussion coming up.

Tip

It's a good practice to wrap most work with streams in using statements. Wrapping the StreamWriter or StreamReader in a using statement, for example, has the same effect as putting the use of the writer or reader in a try/finally exception-handling block. (See Book I for exceptions.) In fact, the compiler translates the using block into the same code it uses for a try/finally, which guarantees that the resources get cleaned up:

try
{
  // Allocate the resource and use it here.
}
finally
{
  // Close and dispose of the resource here.
}

Warning

After the using block, the StreamWriter no longer exists, and the FileStream object can no longer be accessed. The fs variable still exists, assuming that you created the stream outside the using statement, rather than on the fly like this:

using(StreamWriter sw = new StreamWriter(new FileStream(...)) ...

Flushing and closing the writer has flushed and closed the stream as well. If you try to carry out operations on the stream, you get an exception telling you that you can't access a closed object. Notice that in the FileWrite code earlier in this section I nulled the FileStream object, fs, after the using block to ensure that I won't try to use fs again. After that, the FileStream object is handed off to the garbage collector.

Of course, the file you wrote to disk exists. Create and open a new file stream to the file if you need to work with it again.

Note

Specifically, using is aimed at managing cleanup of objects that implement the IDisposable interface (see Book II for information on interfaces). The using statement ensures that the object's Dispose() method gets called. Classes that implement IDisposable guarantee that they have a Dispose() method. IDisposable is mainly about disposing non-.NET resources, mainly stuff in the outside world of the Windows operating system, such as file handles and graphics resources. FileStream, for example, wraps a Windows file handle that must be released, which is why I mention IDisposable here. (Many classes and structs implement IDisposable; your classes can too, if necessary.)

I don't go into IDisposable in this book, but you should plan to become more familiar with it as your C# powers grow. Implementing it correctly has to do with the kind of indeterminate garbage disposal that I mention briefly in Book II and can be complex. So using is for use with classes and structs that implement IDisposable, something that you can check in Help. It won't help you with just any old kind of object. Note: The intrinsic C# types — int, double, char, and such — do not implement IDisposable. Class TextWriter, the base class for StreamWriter, does implement the interface. In Help, that looks like this:

public abstract class TextWriter : MarshalByRefObject, IDisposable

When in doubt, check Help to see if the classes or structs you plan to use implement IDisposable.

Note

You can examine a rewritten version of FileWrite in the FileWriteWithUsing example on the Web. Note that in the rewrite, I had to un-factor the two methods that Main() calls, pulling their code back into Main()inlining it — before I could introduce the using block.

Pulling Them Out of the Stream: Using StreamReader

Writing to a file is cool, but it's sort of worthless if you can't read the file back later. The following FileRead program puts the input back into the phrase file I/O. This program reads a text file like the ones created by FileWrite or by Notepad — it's sort of FileWrite in reverse (note that I don't use using in this one):

Note

// FileRead -- Read a text file and write it out to the Console.
using System;
using System.IO;

namespace FileRead
{
  public class Program
  {
    public static void Main(string[] args)
    {
      // You need a file reader object.
      StreamReader sr = null;
      string fileName = "";

      try
      {
        // Get a filename from the user.
        sr = GetReaderForFile(fileName);

        // Read the contents of the file.
        ReadFileToConsole(sr);
      }
      catch (IOException ioErr)
      {
        //TODO: Before release, replace this with a more user friendly message.
        Console.WriteLine("{0}

", ioErr.Message);
      }
      finally  // Clean up.
      {
if (sr != null) // Guard against trying to Close()a null object.
        {
          sr.Close();   // Takes care of flush as well
          sr = null;
        }
      }

      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }

    // GetReaderForFile -- Open the file and return a StreamReader for it.
    private static StreamReader GetReaderForFile(string fileName)
    {
      StreamReader sr;
      // Enter input filename.
      Console.Write("Enter the name of a text file to read:");
      fileName = Console.ReadLine();

      // User didn't enter anything; throw an exception
      // to indicate that this is not acceptable.
      if (fileName.Length == 0)
      {
         throw new IOException("You need to enter a filename.");
      }

      // Got a name -- open a file stream for reading; don't create the
      // file if it doesn't already exist.
      FileStream fs = File.Open(fileName, FileMode.Open, FileAccess.Read);

      // Wrap a StreamReader around the stream -- this will use
      // the first three bytes of the file to indicate the
      // encoding used (but not the language).
      sr = new StreamReader(fs, true);
      return sr;
    }

    // ReadFileToConsole -- Read lines from the file represented
    //    by sr and write them out to the console.
    private static void ReadFileToConsole(StreamReader sr)
    {
      Console.WriteLine("
Contents of file:");
           // Read one line at a time.
           while(true)
           {
             // Read a line.
             string input = sr.ReadLine();

             // Quit when you don't get anything back.
             if (input == null)
             {
               break;
             }

             // Write whatever you read to the console.
             Console.WriteLine(input);
           }
    }
  }
}

Warning

Recall that the current directory that FileRead uses is the inDebug subdirectory under your FileRead project (not the inDebug directory under the FileWrite program's directory, which is where you used FileWrite to create some test files in the preceding section). Before you run FileRead to try it out, place any plain text file (.TXT extension) in FileRead's inDebug directory and note its name so you can open it. A copy of the TestFile1.txt file created in the FileWrite example would be good.

In FileRead, the user reads one and only one file. The user must enter a valid filename for the program to output. No second chances. After the program has read the file, it quits. If the user wants to peek into a second file, she'll have to run the program again. That's a design choice you might make differently.

The program starts out with all of its serious code wrapped in an exception handler. In the try block, this handler tries to call two methods, first to get a StreamReader for the file and then to read the file and dump its lines to the console. In the event of an exception, the catch block writes the exception message. Finally, whether the exception occurred or not, the finally block makes sure the stream and its file are closed and the variable sr is nulled so the garbage collector can reclaim it (see Book II). I/O exceptions could occur in either method called from the try block. These percolate up to Main() looking for a handler. (No need for exception handlers in the methods.)

Tip

Note the //TODO: comment in the catch block. This is a reminder to make the message more user-friendly before releasing the program. Comments marked this way appear in the Visual Studio Task List window. In that window, select Comments from the drop-down list at the upper left. Double-click an item there to open the editor to that comment in the code.

Note

Because the variable sr is used inside an exception block, you have to set it to null initially — otherwise, the compiler complains about using an uninitialized variable in the exception block. Likewise, check whether sr is already (or still) null before trying to call its Close() method. Better still, convert the program to use using.

Within the GetReaderForFile() method, the program gives the user one chance to enter a filename. If the name of the file entered at the console is nothing but a blank, the program throws its own error message: You need to enter a filename. If the filename isn't empty, it's used to open a FileStream object in read mode. The File.Open() call here is the same as the one used in FileWrite:

  • The first argument is the name of the file.

  • The second argument is the file mode. The mode FileMode.Open says, "Open the file if it exists, and throw an exception if it doesn't." The other option is OpenNew, which creates a zero-length file if the file doesn't exist. Personally, I never saw the need for that mode (who wants to read from an empty file?), but each to his own is what I say.

  • The final argument indicates that I want to read from this FileStream. The other alternatives are Write and ReadWrite. (It would also seem a bit odd to open a file with FileRead using the Write mode, don't you think?)

The resulting FileStream object fs is then wrapped in a StreamReader object sr to provide convenient methods for accessing the text file. The StreamReader is finally passed back to Main() for use.

When the file-open process is done, the FileRead program calls the ReadFileToConsole() method, which loops through the file reading lines of text using the ReadLine() call. The program echoes each line to the console with the ubiquitous Console.WriteLine() call before heading back up to the top of the loop to read another line of text. The ReadLine() call returns a null when the program reaches the end of the file. When this happens, the method breaks out of the read loop and then returns. Main() then closes the object and terminates. (You might say that the reading part of this reader program is wrapped within a while loop inside a method that's in a try block wrapped in an enigma.)

The catch block in Main() exists to keep the exception from propagating up the food chain and aborting the program. If the program throws an exception, I have the catch block write a message and then simply swallow (ignore) the error. You're in Main(), so there's nowhere to rethrow the exception to and nothing to do but close the stream and close up shop. The catch is there to let the user know why the program failed and to prevent an unhandled exception. You could have the program loop back up and ask for a different filename, but this program is so small that it's simpler to let the user run it again.

Tip

Providing an exception handler with a catch block that swallows the exception keeps a program from aborting over an unimportant error. However, use this technique and swallow the exception only if an error would be truly, no fake, nondamaging. See the more extensive discussion in Book II.

Here are a few sample runs:

Enter the name of a text file to read:yourfile.txt
Could not find file 'C:C#ProgramsFileReadinDebugyourfile.txt'.

Press Enter to terminate...

Enter the name of a text file to read:
You need to enter a filename.

Pres Enter to terminate...

Enter the name of a text file to read:myfile.txt

Contents of file:
Dave?
What are you doing, Dave?
Press Enter to terminate...

Tip

For an example of reading arbitrary bytes from a file — which could be either binary or text — see the LoopThroughFiles example in Book I, Chapter 7. The program actually loops through all files in a target directory, reading each file and dumping its contents to the console, so it gets tedious if there are lots of files. Feel free to terminate it by pressing Ctrl+C or by clicking the console window's close box. See the discussion of BinaryReader in the next section.

More Readers and Writers

Earlier in this chapter, I show you the StreamReader and StreamWriter classes that you'll probably use for the bulk of your I/O needs. However, .NET also makes several other reader/writer pairs available:

  • BinaryReader/BinaryWriter: A pair of stream classes that contain methods for reading and writing each value type: ReadChar(), WriteChar(), ReadByte(), WriteByte(), and so on. (These classes are a little more primitive: They don't offer ReadLine()/WriteLine() methods.) The classes are useful for reading or writing an object in binary (nonhuman-readable) format, as opposed to text. You can use an array of bytes to work with the binary data as raw bytes. For example, you may need to read or write the bytes that make up a bitmap graphics file.

    Experiment: Open a file with a .EXE extension using Notepad. You may see some readable text in the window, but most of it looks like some sort of garbage. That's binary data.

    The article "Converting Between Byte and Char Arrays" on my Web site gives you a brief tour of working with arrays of bytes or chars. Book II includes an example, mentioned earlier, that reads binary data. The example uses a BinaryReader with a FileStream object to read chunks of bytes from a file and then writes out the data on the console in hexadecimal (base 16) notation, which I explain in that chapter. Although it wraps a FileStream in the more convenient BinaryReader, that example could just as easily have used the FileStream itself. The reads are identical. While the BinaryReader brings nothing to the table in that example, I used it there to provide an example of this reader. The example does illustrate reading raw bytes into a buffer (an array big enough to hold the bytes read).

  • StringReader/StringWriter: And now for something a little more exotic: simple reader and writer classes that are limited to reading and writing strings. They let you treat a string like a file, an alternative to accessing a string's characters in the usual ways, such as with a foreach loop

    foreach(char c in someString) { Console.Write(c); }

    or with array-style bracket notation ([ ])

    char c = someString[3];

    or with String methods like Split(), Concatenate(), and IndexOf(). With StringReader/StringWriter, you read from and write to a string much as you would to a file. This technique is useful for long strings with hundreds or thousands of characters that you want to process in bunches, and it provides a handy way to work with a StringBuilder.

    When you create a StringReader, you initialize it with a string to read. When you create a StringWriter, you can pass a StringBuilder object to it or create it empty. Internally, the StringWriter stores a StringBuilder — either the one you passed to its constructor or a new, empty one. You can get at the internal StringBuilder's contents by calling StringWriter's ToString() method.

    Each time you read from the string (or write to it), the "file pointer" advances to the next available character past the read or write. Thus, as with file I/O, you have the notion of a "current position." When you read, say, 10 characters from a 1,000-character string, the position is set to the eleventh character after the read.

    The methods in these classes parallel those described earlier for the StreamReader and StreamWriter classes. If you can use those, you can use these.

    Note

    The StringReadingAndWriting example on the Web illustrates using StringReader and StringWriter, including a few quirks to watch for.

Exploring More Streams than Lewis and Clark

I should mention, before meandering on, that file streams are not the only kinds of Stream classes available. The flood of Stream classes includes (but probably is not limited to) those in the following list. Note that unless I specify otherwise, these stream classes all live in the System.IO namespace.

  • FileStream: For reading and writing files on a disk.

  • Note

    MemoryStream: Manages reading and writing data to a block of memory. I use this technique sometimes in unit tests, to avoid actually interacting with the (slow, possibly troublesome) file system. In this way, I can "fake" a file when testing code that reads and writes. See my Web site for an illustration of this technique. (I'll leave a breadcrumb there.) And see some brief notes on MemoryStream on the Web, in the MemoryStreamSpike example.

    Note that the StringReader/StringWriter classes discussed in the preceding section can be useful in unit testing in much the same way as with MemoryStream. I prefer StringReader/StringWriter for that purpose. The StringReadingAndWriting example on the Web illustrates the technique with some simple unit tests.

  • Note

    BufferedStream: Buffering is a technique for speeding up input/output operations by reading or writing bigger chunks of data at a time. Lots of small reads or writes mean lots of slow disk access — but if you read a much bigger chunk than you need now, you can then continue to read your small chunks out of the buffer — which is far faster than reading the disk. When a BufferedStream's underlying buffer runs out of data, it reads in another big chunk — maybe even the whole file. Buffered writing is similar.

    Class FileStream automatically buffers its operations, so BufferedStream is for special cases, such as working with a NetworkStream to read and write bytes over a network. In this case, you wrap the BufferedStream around the NetworkStream, effectively "chaining" streams. When you write to the BufferedStream, it writes to the underlying NetworkStream, and so on.

    When you're wrapping one stream around another, you're composing streams. (You can look it up in the Help index for more information.) I discuss wrapping in the earlier sidebar "Wrap my fish in newspaper."

  • NetworkStream: Manages reading and writing data over a network. See BufferedStream for a simplified discussion of using it. NetworkStream is in the System.Net.Sockets namespace because it uses a technology called sockets to make connections across a network.

  • UnmanagedMemoryStream: Lets you read and write data in "unmanaged" blocks of memory. Unmanaged means, basically, "not .NET" and not managed by the .NET runtime and its garbage collector. This is advanced stuff, dealing with interaction between .NET code and code written under the Windows operating system.

  • CryptoStream: Located in the System.Security.Cryptography namespace, this stream class lets you pass data to and from an encryption or decryption transformation. I'm sure you'll use it daily. I know I do.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.217.194