File and Stream I/O

Classes in the System.IO namespace enable managed applications to perform file I/O and other forms of input and output. The fundamental building block for managed I/O is the stream, which is an abstract representation of byte-oriented data. Streams are represented by the System.IO.Stream class. Because Stream is abstract, System.IO as well as other namespaces include concrete classes derived from Stream that represent physical data sources. For example, System.IO.FileStream permits files to be accessed as streams; System.IO.MemoryStream does the same for blocks of memory. The System.Net.Sockets namespace includes a Stream derivative named NetworkStream that abstracts sockets as streams, and the System.Security.Cryptography namespace defines a CryptoStream class used to read and write encrypted streams.

Stream classes have methods that you can call to perform input and output, but the .NET Framework offers an additional level of abstraction in the form of readers and writers. The BinaryReader and BinaryWriter classes provide an easy-to-use interface for performing binary reads and writes on stream objects. StreamReader and StreamWriter, which derive from the abstract TextReader and TextWriter classes, support the reading and writing of text.

One of the most common forms of I/O that managed and unmanaged applications alike are called upon to perform is file I/O. The general procedure for reading and writing files in a managed application is as follows:

  1. Open the file using a FileStream object.

  2. For binary reads and writes, wrap instances of BinaryReader and BinaryWriter around the FileStream object and call BinaryReader and BinaryWriter methods such as Read and Write to perform input and output.

  3. For reads and writes involving text, wrap a StreamReader and StreamWriter around the FileStream object and use StreamReader and StreamWriter methods such as ReadLine and WriteLine to perform input and output.

  4. Close the FileStream object.

That this example deals specifically with file I/O is not to imply that readers and writers are only for files. They鈥檙e not. Later in this chapter, you鈥檒l see a sample program that uses a StreamReader object to read text fetched from a Web page. The fact that readers and writers work with any kind of Stream object makes them powerful tools for performing I/O on any stream-oriented media.

System.IO also contains classes for manipulating files and directories. The File class provides static methods for opening, creating, copying, moving, and renaming files, as well as for reading and writing file attributes. FileInfo boasts the same capabilities, but FileInfo exposes its features through instance methods rather than static methods. The Directory and DirectoryInfo classes provide a programmatic interface to directories, enabling them to be created, deleted, enumerated, and more via simple method calls. Chapter 4鈥檚 ControlDemo application demonstrates how to use File and Directory methods to enumerate the files in a directory and obtain information about those files.

Text File I/O

The reading and writing of text files from managed applications is aided and abetted by the FileStream, StreamReader, and StreamWriter classes. Suppose you wanted to write a simple app that dumps text files to the console window鈥攖he functional equivalent of the old DOS TYPE command. Here鈥檚 how to go about it:

StreamReader聽reader聽=聽new聽StreamReader聽(filename);
for聽(string聽line聽=聽reader.ReadLine聽();聽line聽!=聽null;line聽=聽reader.ReadLine聽())
聽聽聽聽Console.WriteLine聽(line);
reader.Close聽();

The first line creates a StreamReader object that wraps a FileStream created from filename. The for loop uses StreamReader.ReadLine to iterate through the lines in the file and Console.WriteLine to output them to the console window. The final statement closes the file by closing the StreamReader.

That鈥檚 the general approach, but in real life you have to anticipate the possibility that things might not go strictly according to plan. For example, what if the file name passed to StreamReader鈥檚 constructor is invalid? Or what if the framework throws an exception before the final statement is executed, causing the file to be left open? Figure 3-1 contains the source code for a managed version of the TYPE command (called LIST to distinguish it from the real TYPE command) that responds gracefully to errors using C# exception handling. The catch block traps exceptions thrown when StreamReader鈥檚 constructor encounters an invalid file name or when I/O errors occur as the file is being read. The finally block ensures that the file is closed even if an exception is thrown.

List.cs
using聽System;
using聽System.IO;

class聽MyApp
{
聽聽聽聽static聽void聽Main聽(string[]聽args)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽Make聽sure聽a聽file聽name聽was聽entered聽on聽the聽command聽line
聽聽聽聽聽聽聽聽if聽(args.Length聽==聽0)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Error:聽Missing聽file聽name");
聽聽聽聽聽聽聽聽聽聽聽聽return;
聽聽聽聽聽聽聽聽}

聽聽聽聽聽聽聽聽//聽Open聽the聽file聽and聽display聽its聽contents
聽聽聽聽聽聽聽聽StreamReader聽reader聽=聽null;

聽聽聽聽聽聽聽聽try聽{
聽聽聽聽聽聽聽聽聽聽聽聽reader聽=聽new聽StreamReader聽(args[0]);
聽聽聽聽聽聽聽聽聽聽聽聽for聽(string聽line聽=聽reader.ReadLine聽();聽line聽!=聽null;
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽line聽=聽reader.ReadLine聽())
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽(line);
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽catch聽(IOException聽e)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽(e.Message);
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽finally聽{
聽聽聽聽聽聽聽聽聽聽聽聽if聽(reader聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽reader.Close聽();
聽聽聽聽聽聽聽聽}
聽聽聽聽}
}
Figure 3-1
A managed application that mimics the TYPE command.

Because the FCL is such a comprehensive class library, passing a file name to StreamReader鈥檚 constructor isn鈥檛 the only way to open a text file for reading. Here are some others:

//聽Use聽File.Open聽to聽create聽a聽FileStream,聽and聽then聽wrap聽a
//聽StreamReader聽around聽it
FileStream聽stream聽=聽File.Open聽(filename,聽FileMode.Open,聽FileAccess.Read);
StreamReader聽reader聽=聽new聽StreamReader聽(stream);
//聽Create聽a聽FileStream聽directly,聽and聽then聽wrap聽a
//聽StreamReader聽around聽it
FileStream聽stream聽=聽new聽FileStream聽(filename,聽FileMode.Open,聽FileAccess.Read);
StreamReader聽reader聽=聽new聽StreamReader聽(stream);
//聽Use聽File.OpenText聽to聽create聽a聽FileStream聽and聽a
//聽StreamReader聽in聽one聽step
StreamReader聽reader聽=聽File.OpenText聽(filename);

There are other ways, too, but you get the picture. None of these methods for wrapping a StreamReader around a file is intrinsically better than the others, but they do demonstrate the numerous ways in which ordinary, everyday tasks can be accomplished using the .NET Framework class library.

StreamReaders read from text files; StreamWriters write to them. Suppose you wanted to write catch handlers that log exceptions to a text file. Here鈥檚 a LogException method that takes a file name and an Exception object as input and uses StreamWriter to append the error message in the Exception object to the file:

void聽LogException聽(string聽filename,聽Exception聽ex)
{
聽聽聽聽StreamWriter聽writer聽=聽null;
聽聽聽聽try聽{
聽聽聽聽聽聽聽聽writer聽=聽new聽StreamWriter聽(filename,聽true);
聽聽聽聽聽聽聽聽writer.WriteLine聽(ex.Message);
聽聽聽聽}
聽聽聽聽finally聽{
聽聽聽聽聽聽聽聽if聽(writer聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽writer.Close聽();
聽聽聽聽}
}

Passing true in the second parameter to StreamWriter鈥檚 constructor tells the StreamWriter to append data if the file exists and to create a new file if it doesn鈥檛.

Binary File I/O

BinaryReader and BinaryWriter are to binary files as StreamReader and StreamWriter are to text files. Their key methods are Read and Write, which do exactly what you would expect them to. To demonstrate, the sample program in Figure 3-2 uses BinaryReader and BinaryWriter to encrypt and unencrypt files by XORing their contents with passwords entered on the command line. Encrypting a file is as simple as running Scramble.exe from the command line and including a file name and password, in that order, as in:

scramble聽readme.txt聽imbatman

To unencrypt the file, execute the same command again:

scramble聽readme.txt聽imbatman

XOR-encryption is hardly industrial-strength encryption, but it鈥檚 sufficient to hide file contents from casual intruders. And it鈥檚 simple enough to not distract from the main point of the application, which is to get a firsthand look at BinaryReader and BinaryWriter.

Scramble.cs contains two lines of code that merit further explanation:

ASCIIEncoding聽enc聽=聽new聽ASCIIEncoding聽();
byte[]聽keybytes聽=聽enc.GetBytes聽(key);

These statements convert the second command-line parameter鈥攁 string representing the encryption key鈥攊nto an array of bytes. Strings in the .NET Framework are instances of System.String. ASCIIEncoding.GetBytes is a convenient way to convert a System.String into a byte array. Scramble XORs the bytes in the file with the bytes in the converted string. Had the program used UnicodeEncoding.GetBytes instead, encryption would be less effective because calling UnicodeEncoding.GetBytes on strings containing characters from Western alphabets produces a buffer in which every other byte is a 0. XORing a byte with 0 does absolutely nothing, and XOR encryption is weak enough as is without worsening matters by using keys that contain lots of zeros. ASCIIEncoding is a member of the System.Text namespace, which explains the using System.Text directive at the top of the file.

Scramble.cs
using聽System;
using聽System.IO;
using聽System.Text;

class聽MyApp
{
聽聽聽聽const聽int聽bufsize聽=聽1024;

聽聽聽聽static聽void聽Main聽(string[]聽args)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽Make聽sure聽a聽file聽name聽and聽encryption聽key聽were聽entered
聽聽聽聽聽聽聽聽if聽(args.Length聽<聽2)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Syntax:聽SCRAMBLE聽filename聽key");
聽聽聽聽聽聽聽聽聽聽聽聽return;
聽聽聽聽聽聽聽聽}

聽聽聽聽聽聽聽聽string聽filename聽=聽args[0];
聽聽聽聽聽聽聽聽string聽key聽=聽args[1];
聽聽聽聽聽聽聽聽FileStream聽stream聽=聽null;

聽聽聽聽聽聽聽聽try聽{
聽聽聽聽聽聽聽聽聽聽聽聽//聽Open聽the聽file聽for聽reading聽and聽writing
Figure 3-2
A simple file encryption utility.
聽聽聽聽聽聽聽聽聽聽聽聽stream聽=聽File.Open聽(filename,聽FileMode.Open,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽FileAccess.ReadWrite);

聽聽聽聽聽聽聽聽聽聽聽聽//聽Wrap聽a聽reader聽and聽writer聽around聽the聽FileStream
聽聽聽聽聽聽聽聽聽聽聽聽BinaryReader聽reader聽=聽new聽BinaryReader聽(stream);
聽聽聽聽聽聽聽聽聽聽聽聽BinaryWriter聽writer聽=聽new聽BinaryWriter聽(stream);

聽聽聽聽聽聽聽聽聽聽聽聽//聽Convert聽the聽key聽into聽a聽byte聽array
聽聽聽聽聽聽聽聽聽聽聽聽ASCIIEncoding聽enc聽=聽new聽ASCIIEncoding聽();
聽聽聽聽聽聽聽聽聽聽聽聽byte[]聽keybytes聽=聽enc.GetBytes聽(key);

聽聽聽聽聽聽聽聽聽聽聽聽//聽Allocate聽an聽I/O聽buffer聽and聽a聽key聽buffer
聽聽聽聽聽聽聽聽聽聽聽聽byte[]聽buffer聽=聽new聽byte[bufsize];
聽聽聽聽聽聽聽聽聽聽聽聽byte[]聽keybuf聽=聽new聽byte[bufsize聽+聽keybytes.Length聽-聽1];

聽聽聽聽聽聽聽聽聽聽聽聽//聽Replicate聽the聽byte聽array聽in聽the聽key聽buffer聽to聽create
聽聽聽聽聽聽聽聽聽聽聽聽//聽an聽encryption聽key聽whose聽size聽equals聽or聽exceeds聽the
聽聽聽聽聽聽聽聽聽聽聽聽//聽size聽of聽the聽I/O聽buffer
聽聽聽聽聽聽聽聽聽聽聽聽int聽count聽=聽(1024聽+聽keybytes.Length聽-聽1)聽/聽keybytes.Length;
聽聽聽聽聽聽聽聽聽聽聽聽for聽(int聽i=0;聽i<count;聽i++)聽聽聽聽聽聽聽聽聽聽聽聽
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Array.Copy聽(keybytes,聽0,聽keybuf,聽i聽*聽keybytes.Length,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽keybytes.Length);

聽聽聽聽聽聽聽聽聽聽聽聽//聽Read聽the聽file聽in聽bufsize聽blocks,聽XOR-encrypt聽each聽block,
聽聽聽聽聽聽聽聽聽聽聽聽//聽and聽write聽the聽encrypted聽block聽back聽to聽the聽file
聽聽聽聽聽聽聽聽聽聽聽聽long聽lBytesRemaining聽=聽stream.Length;

聽聽聽聽聽聽聽聽聽聽聽聽while聽(lBytesRemaining聽>聽0)聽{
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽long聽lPosition聽=聽stream.Position;
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽int聽nBytesRequested聽=聽(int)聽System.Math.Min聽(bufsize,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽lBytesRemaining);
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽int聽nBytesRead聽=聽reader.Read聽(buffer,聽0,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽nBytesRequested);

聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽for聽(int聽i=0;聽i<nBytesRead;聽i++)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽buffer[i]聽^=聽keybuf[i];									

聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽stream.Seek聽(lPosition,聽SeekOrigin.Begin);
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽writer.Write聽(buffer,聽0,聽nBytesRead);
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽lBytesRemaining聽-=聽nBytesRead;
聽聽聽聽聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽catch聽(Exception聽e)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽(e.Message);
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽finally聽{
聽聽聽聽聽聽聽聽聽聽聽聽if聽(stream聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽stream.Close聽();
聽聽聽聽聽聽聽聽}
聽聽聽聽}
}