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 FileStream
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 Shop. 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.
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
.
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
in the “Exploring More Streams than Lewis and Clark” section, later in this 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.
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.
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.”
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 encoding 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.
// 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 = GetWriterForFile( fileName );
// Read one string at a time, outputting each to the
// FileStream open for writing.
WriteFileFromConsole( 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.
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 5 in Book I 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 directory
filename.txt
, is assumed to be the full path on the local machine. Filenames that start with two slashes — for example, \
your machine
some directory
filename.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.
In the next-to-last code 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.
Finally, you’re writing!
After setting up its StreamWriter
, the FileWrite
program begins reading lines of text 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.
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.)
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.
Upon encountering the end of the while
loop, either by successfully 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.
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.
try
{
// Allocate the resource and use it here.
}
finally
{
// Close and dispose of the resource here.
}
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.
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 struct
s 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 struct
s you plan to use implement IDisposable
.
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):
// 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);
}
}
}
}
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.)
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.
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...
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 byte
s 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 website gives you a brief tour of working with arrays of byte
s or char
s. Chapter 7 in Book I 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 use 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 string
s. 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 (such as an entire text file read into a string
) 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.
The StringReadingAndWritingStringWritingAndReading
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.
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 website 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.
StringWritingAndReading
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.