The earlier chapters of this book have used parts of the C# I/O system, such as Console.WriteLine( ), but have done so without much formal explanation. Because the I/O system is built upon a hierarchy of classes, it was not possible to present its theory and details without first discussing classes, inheritance, and exceptions. Now it is time to examine I/O in detail. Because C# uses the I/O system and classes defined by the .NET Framework, a discussion of I/O under C# is also a discussion of the.NET I/O system, in general.
This chapter examines both console I/O and file I/O. Be forewarned that the I/O system is quite large. This chapter describes the most important and commonly used features.
C# programs perform I/O through streams. A stream is an abstraction that either produces or consumes information. A stream is linked to a physical device by the I/O system. All streams behave in the same manner, even if the actual physical devices they are linked to differ. Thus, the I/O classes and methods can be applied to many types of devices. For example, the same methods that you use to write to the console can also be used to write to a disk file.
At the lowest level, all C# I/O operates on bytes. This makes sense because many devices are byte oriented when it comes to I/O operations. Frequently, though, we humans prefer to communicate using characters. Recall that in C#, char is a 16-bit type, and byte is an 8-bit type. If you are using the ASCII character set, then it is easy to convert between char and byte; just ignore the high-order byte of the char value. But this won’t work for the rest of the Unicode characters, which need both bytes (and possibly more). Thus, byte streams are not perfectly suited to handling character-based I/O. To solve this problem, the.NET Framework defines several classes that convert a byte stream into a character stream, handling the translation of byte-to-char and char-to-byte for you automatically.
Three predefined streams, which are exposed by the properties called Console.In, Console.Out, and Console.Error, are available to all programs that use the System namespace. Console.Out refers to the standard output stream. By default, this is the console. When you call Console.WriteLine( ), for example, it automatically sends information to Console.Out. Console.In refers to standard input, which is, by default, the keyboard. Console.Error refers to the standard error stream, which is also the console by default. However, these streams can be redirected to any compatible I/O device. The standard streams are character streams. Thus, these streams read and write characters.
The.NET Framework defines both byte and character stream classes. However, the character stream classes are really just wrappers that convert an underlying byte stream to a character stream, handling any conversion automatically. Thus, the character streams, although logically separate, are built upon byte streams.
The core stream classes are defined within the System.IO namespace. To use these classes, you will usually include the following statement near the top of your program:
using System.IO;
The reason that you don’t have to specify System.IO for console input and output is that the Console class is defined in the System namespace.
The core stream class is System.IO.Stream. Stream represents a byte stream and is a base class for all other stream classes. It is also abstract, which means that you cannot instantiate a Stream object. Stream defines a set of standard stream operations. Table 14-1 shows several commonly used methods defined by Stream.
Several of the methods shown in Table 14-1 will throw an IOException if an I/O error occurs. If an invalid operation is attempted, such as attempting to write to a stream that is read-only, a NotSupportedException is thrown. Other exceptions are possible, depending on the specific method.
Notice that Stream defines methods that read and write data. However, not all streams will support both of these operations, because it is possible to open read-only or write-only streams. Also, not all streams will support position requests via Seek( ). To determine the capabilities of a stream, you will use one or more of Stream’s properties. They are shown in Table 14-2. Also shown are the Length and Position properties, which contain the length of the stream and its current position.
Several concrete byte streams are derived from Stream. Those defined in the System.IO namespace are shown here:
Several other concrete stream classes are also supported by the.NET Framework, which provide support for compressed files, sockets, and pipes, among others. It is also possible for you to derive your own stream classes. However, for the vast majority of applications, the built-in streams will be sufficient.
To create a character stream, wrap a byte stream inside one of the character stream wrappers. At the top of the character stream hierarchy are the abstract classes TextReader and TextWriter. TextReader handles input, and TextWriter handles output. The methods defined by these two abstract classes are available to all of their subclasses. Thus, they form a minimal set of I/O functions that all character streams will have.
Table 14-3 shows the input methods in TextReader. In general, these methods can throw an IOException on error. (Some can throw other types of exceptions, too.) Of particular interest is the ReadLine( ) method, which reads an entire line of text, returning it as a string. This method is useful when reading input that contains embedded spaces. TextReader also specifies the Close( ) method, shown here:
void Close( )
It closes the reader and frees its resources.
TextWriter defines versions of Write( ) and WriteLine( ) that output all of the built-in types. For example, here are just a few of their overloaded versions:
All throw an IOException if an error occurs while writing.
TextWriter also specifies the Close( ) and Flush( ) methods shown here:
virtual void Close( )
virtual void Flush( )
Flush( ) causes any data remaining in the output buffer to be written to the physical medium. Close( ) closes the writer and frees its resources.
The TextReader and TextWriter classes are implemented by several character-based stream classes, including those shown here. Thus, these streams provide the methods and properties specified by TextReader and TextWriter.
In addition to the byte and character streams, there are two binary stream classes that can be used to read and write binary data directly. These streams are called BinaryReader and BinaryWriter. We will look closely at these later in this chapter when binary file I/O is discussed.
Now that you understand the general layout of the I/O system, the rest of this chapter will examine its various pieces in detail, beginning with console I/O.
Console I/O is accomplished through the standard streams Console.In, Console.Out, and Console.Error. Console I/O has been used since Chapter 2, so you are already familiar with it. As you will see, it has some additional capabilities.
Before we begin, however, it is important to emphasize a point made earlier in this book: Most real applications of C# will not be text-based, console programs. Rather, they will be graphically oriented programs or components that rely upon a windowed interface for interaction with the user, or will be server-side code. Thus, the portion of the I/O system that relates to console input and output is not widely used. Although text-based programs are excellent as teaching examples, for short utility programs, and for some types of components, they are not suitable for most real-world applications.
Console.In is an instance of TextReader, and you can use the methods and properties defined by TextReader to access it. However, you will usually use the methods provided by Console, which automatically read from Console.In. Console defines three input methods. The first two, Read( ) and ReadLine( ), have been available since.NET Framework 1.0. The third, ReadKey( ), was added by.NET Framework 2.0.
To read a single character, use the Read( ) method:
static int Read( )
Read( ) returns the next character read from the console. It waits until the user presses a key and then returns the result. The character is returned as an int, which must be cast to char. Read( ) returns –1 on error. This method will throw an IOException on failure. When using Read( ), console input is line-buffered, so you must press ENTER before any character that you type will be sent to your program.
Here is a program that reads a character from the keyboard using Read( ):
// Read a character from the keyboard.
using System;
class KbIn {
static void Main() {
char ch;
Console.Write("Press a key followed by ENTER: ");
ch = (char) Console.Read(); // get a char
Console.WriteLine("Your key is: " + ch);
}
}
Here is a sample run:
Press a key followed by ENTER: t
Your key is: t
The fact that Read( ) is line-buffered is a source of annoyance at times. When you press ENTER, a carriage-return, line-feed sequence is entered into the input stream. Furthermore, these characters are left pending in the input buffer until you read them. Thus, for some applications, you may need to remove them (by reading them) before the next input operation. (To read keystrokes from the console in a non-line-buffered manner, you can use ReadKey( ), described later in this section.)
To read a string of characters, use the ReadLine( ) method. It is shown here:
static string ReadLine( )
ReadLine( ) reads characters until you press ENTER and returns them in a string object. This method will throw an IOException if an I/O error occurs.
Here is a program that demonstrates reading a string from Console.In by using ReadLine( ):
// Input from the console using ReadLine().
using System;
class ReadString {
static void Main() {
string str;
Console.WriteLine("Enter some characters.");
str = Console.ReadLine();
Console.WriteLine("You entered: " + str);
}
}
Here is a sample run:
Enter some characters.
This is a test.
You entered: This is a test.
Although the Console methods are the easiest way to read from Console.In, you can call methods on the underlying TextReader. For example, here is the preceding program rewritten to use the ReadLine( ) method defined by TextReader:
// Read a string from the keyboard, using Console.In directly.
using System;
class ReadChars2 {
static void Main() {
string str;
Console.WriteLine("Enter some characters.");
str = Console.In.ReadLine(); // call TextReader's ReadLine() method
Console.WriteLine("You entered: " + str);
}
}
Notice how ReadLine( ) is now invoked directly on Console.In. The key point here is that if you need access to the methods defined by the TextReader that underlies Console.In, you will invoke those methods as shown in this example.
The.NET Framework includes a method in Console that enables you to read individual keystrokes directly from the keyboard, in a non-line-buffered manner. This method is called ReadKey( ). When it is called, it waits until a key is pressed. When a key is pressed, ReadKey( ) returns the keystroke immediately. The user does not need to press ENTER. Thus, ReadKey( ) allows keystrokes to be read and processed in real time.
ReadKey( ) has these two forms:
static ConsoleKeyInfo ReadKey( )
static ConsoleKeyInfo ReadKey(bool intercept)
The first form waits for a key to be pressed. When that occurs, it returns the key and also displays the key on the screen. The second form also waits for and returns a keypress. However, if intercept is true, then the key is not displayed. If intercept is false, the key is displayed.
ReadKey( ) returns information about the keypress in an object of type ConsoleKeyInfo, which is a structure. It contains the following read-only properties:
char KeyChar
ConsoleKey Key
ConsoleModifiers Modifiers
KeyChar contains the char equivalent of the character that was pressed. Key contains a value from the ConsoleKey enumeration, which is an enumeration of all the keys on the keyboard. Modifiers describes which, if any, of the keyboard modifiers ATL, CTRL, or SHIFT were pressed when the keystroke was generated. These modifiers are represented by the ConsoleModifiers enumeration, which has these values: Control, Shift, and Alt. More than one modifier value might be present in Modifiers.
The major advantage to ReadKey( ) is that it provides a means of achieving interactive keyboard input because it is not line buffered. To see this effect, try the following program:
// Read keystrokes from the console by using ReadKey().
using System;
class ReadKeys {
static void Main() {
ConsoleKeyInfo keypress;
Console.WriteLine("Enter keystrokes. Enter Q to stop.");
do {
keypress = Console.ReadKey(); // read keystrokes
Console.WriteLine(" Your key is: " + keypress.KeyChar);
// Check for modifier keys.
if((ConsoleModifiers.Alt & keypress.Modifiers) != 0)
Console.WriteLine("Alt key pressed.");
if((ConsoleModifiers.Control & keypress.Modifiers) != 0)
Console.WriteLine("Control key pressed.");
if((ConsoleModifiers.Shift & keypress.Modifiers) != 0)
Console.WriteLine("Shift key pressed.");
} while(keypress.KeyChar != 'Q');
}
}
Here is a sample run:
Enter keystrokes. Enter Q to stop.
a Your key is: a
b Your key is: b
d Your key is: d
A Your key is: A
Shift key pressed.
B Your key is: B
Shift key pressed.
C Your key is: C
Shift key pressed.
• Your key is: •
Control key pressed.
Q Your key is:Q
Shift key pressed.
As the output confirms, each time a key is pressed, ReadKey( ) immediately returns the keypress. As explained, this differs from Read( ) and ReadLine( ), which use line-buffered input. Therefore, if you want to achieve interactive responses from the keyboard, use ReadKey( ).
Console.Out and Console.Error are objects of type TextWriter. Console output is most easily accomplished with Write( ) and WriteLine( ), with which you are already familiar. Versions of these methods exist that output each of the built-in types. Console defines its own versions of Write( ) and WriteLine( ) so they can be called directly on Console, as you have been doing throughout this book. However, you can invoke these (and other) methods on the TextWriter that underlies Console.Out and Console.Error, if you choose.
Here is a program that demonstrates writing to Console.Out and Console.Error. By default, both write to the console.
// Write to Console.Out and Console.Error.
using System;
class ErrOut {
static void Main() {
int a=10, b=0;
int result;
Console.Out.WriteLine("This will generate an exception.");
try {
result = a / b; // generate an exception
} catch(DivideByZeroException exc) {
Console.Error.WriteLine(exc.Message);
}
}
}
The output from the program is shown here:
This will generate an exception.
Attempted to divide by zero.
Sometimes newcomers to programming are confused about when to use Console.Error. Since both Console.Out and Console.Error default to writing their output to the console, why are there two different streams? The answer lies in the fact that the standard streams can be redirected to other devices. For example, Console.Error can be redirected to write to a disk file, rather than the screen. Thus, it is possible to direct error output to a log file, for example, without affecting console output. Conversely, if console output is redirected and error output is not, then error messages will appear on the console, where they can be seen. We will examine redirection later, after file I/O has been described.
The.NET Framework provides classes that allow you to read and write files. Of course, the most common type of files are disk files. At the operating system level, all files are byte oriented. As you would expect, there are methods to read and write bytes from and to a file. Thus, reading and writing files using byte streams is very common. You can also wrap a byte-oriented file stream within a character-based object. Character-based file operations are useful when text is being stored. Character streams are discussed later in this chapter. Byte-oriented I/O is described here.
To create a byte-oriented stream attached to a file, you will use the FileStream class. FileStream is derived from Stream and contains all of Stream’s functionality.
Remember, the stream classes, including FileStream, are defined in System.IO. Thus, you will usually include
using System.IO;
near the top of any program that uses them.
To create a byte stream linked to a file, create a FileStream object. FileStream defines several constructors. Perhaps its most commonly used one is shown here:
FileStream(string path, FileMode mode)
Here, path specifies the name of the file to open, which can include a full path specification. The mode parameter specifies how the file will be opened. It must be one of the values defined by the FileMode enumeration. These values are shown in Table 14-4. In general, this constructor opens a file for read/write access. The exception is when the file is opened using FileMode.Append. In this case, the file is write-only.
If a failure occurs when attempting to open the file, an exception will be thrown. If the file cannot be opened because it does not exist, FileNotFoundException will be thrown. If the file cannot be opened because of some type of I/O error, IOException will be thrown. Other exceptions include ArgumentNullException (the filename is null), ArgumentException (the filename is invalid), ArgumentOutOfRangeException (the mode is invalid), SecurityException (user does not have access rights), PathTooLongException (the filename/path is too long), NotSupportedException (the filename specifies an unsupported device), and DirectoryNotFoundException (specified directory is invalid).
The exceptions PathTooLongException, DirectoryNotFoundException, and FileNotFoundException are subclasses of IOException. Thus, it is possible to catch all three by catching IOException.
There are many ways to handle the process of opening a file. The following shows one way. It opens a file called test.dat for input.
FileStream fin = null;
try {
fin = new FileStream("test", FileMode.Open);
}
catch(IOException exc) { // catch all I/O exceptions
Console.WriteLine(exc.Message);
// Handle the error.
}
catch(Exception exc { // catch any other exception
Console.WriteLine(exc.Message);
// Handle the error, if possible.
// Rethrow those exceptions that you don't handle.
}
Here, the first catch clause handles situations in which the file is not found, the path is too long, the directory does not exist, or other I/O errors occur. The second catch, which is a “catch all” clause for all other types of exceptions, handles the other possible errors (possibly by rethrowing the exception). You could also check for each exception individually, reporting more specifically the problem that occurred and taking remedial action specific to that error.
For the sake of simplicity, the examples in this book will catch only IOException, but your real-world code may (probably will) need to handle the other possible exceptions, depending upon the circumstances. Also, the exception handlers in this chapter simply report the error, but in many cases, your code should take steps to correct the problem when possible. For example, you might reprompt the user for a filename if the one previously entered is not found. You might also need to rethrow the exception.
REMEMBER To keep the code simple, the examples in this chapter catch only IOException, but your own code may need to handle other possible exceptions or handle each type of I/O exception individually.
As mentioned, the FileStream constructor just described opens a file that (in most cases) has read/write access. If you want to restrict access to just reading or just writing, use this constructor instead:
FileStream(string path, FileMode mode, FileAccess access)
As before, path specifies the name of the file to open, and mode specifies how the file will be opened. The value passed in access specifies how the file can be accessed. It must be one of the values defined by the FileAccess enumeration, which are shown here:
For example, this opens a read-only file:
FileStream fin = new FileStream("test.dat", FileMode.Open, FileAccess.Read);
When you are done with a file, you must close it. This can be done by calling Close( ). Its general form is shown here:
void Close( )
Closing a file releases the system resources allocated to the file, allowing them to be used by another file. As a point of interest, Close( ) works by calling Dispose( ), which actually frees the resources.
NOTE The using statement, described in Chapter 20, offers a way to automatically close a file when it is no longer needed. This approach is beneficial in many file-handling situations because it provides a simple means to ensure that a file is closed when it is no longer needed. However, to clearly illustrate the fundamentals of file handling, including the point at which a file can be closed, this chapter explicitly calls Close( ) in all cases.
FileStream defines two methods that read bytes from a file: ReadByte( ) and Read( ). To read a single byte from a file, use ReadByte( ), whose general form is shown here:
int ReadByte( )
Each time it is called, it reads a single byte from the file and returns it as an integer value. It returns –1 when the end of the file is encountered. Possible exceptions include NotSupportedException (the stream is not opened for input) and ObjectDisposedException (the stream is closed).
To read a block of bytes, use Read( ), which has this general form:
int Read(byte[ ] array, int offset, int count)
Read( ) attempts to read up to count bytes into array starting at array [offset]. It returns the number of bytes successfully read. An IOException is thrown if an I/O error occurs. Several other types of exceptions are possible, including NotSupportedException, which is thrown if reading is not supported by the stream.
The following program uses ReadByte( ) to input and display the contents of a text file, the name of which is specified as a command-line argument. Note the program first checks that a filename has been specified before trying to open the file.
/* Display a text file.
To use this program, specify the name of the file that you
want to see. For example, to see a file called TEST.CS,
use the following command line.
ShowFile TEST.CS
*/
using System;
using System.IO;
class ShowFile {
static void Main(string[] args) {
int i;
FileStream fin;
if(args.Length != 1) {
Console.WriteLine("Usage: ShowFile File");
return;
}
try {
fin = new FileStream(args[0], FileMode.Open);
} catch(IOException exc) {
Console.WriteLine("Cannot Open File");
Console.WriteLine(exc.Message);
return; // File can't be opened, so stop the program.
}
// Read bytes until EOF is encountered.
try {
do {
i = fin.ReadByte();
if(i != -1) Console.Write((char) i);
} while(i != -1);
} catch(IOException exc) {
Console.WriteLine("Error Reading File");
Console.WriteLine(exc.Message);
} finally {
fin.Close();
}
}
}
Notice that the program uses two try blocks. The first catches any I/O exceptions that might prevent the file from being opened. If an I/O error occurs, the program terminates. Otherwise, the second try block monitors the read operation for I/O exceptions. Thus, the second try block executes only if fin refers to an open file. Also, notice that the file is closed in the finally block associated with the second try block. This means that no matter how the do loop ends (either normally or because of an error), the file will be closed. Although not an issue in this specific example (because the entire program ends at that point anyway), the advantage to this approach, in general, is that if the code that accesses a file terminates because of some exception, the file is still closed by the finally block. This ensures that the file is closed in all cases.
In some situations, it may be easier to wrap the portions of a program that open the file and access the file within a single try block (rather than separating the two). For example, here is another, shorter way to write the ShowFile program:
// Display a text file. Compact version.
using System;
using System.IO;
class ShowFile {
static void Main(string[] args) {
int i;
FileStream fin = null;
if(args.Length != 1) {
Console.WriteLine("Usage: ShowFile File");
return;
}
// Use a single try block to open the file and then
// read from it.
try {
fin = new FileStream(args[0], FileMode.Open);
// Read bytes until EOF is encountered.
do {
i = fin.ReadByte();
if(i != -1) Console.Write((char) i);
} while(i != -1);
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
if(fin != null) fin.Close();
}
}
}
Notice in this version that the FileStream reference fin is initialized to null. If the file can be opened by the FileStream constructor, fin will be non-null. If the constructor fails, fin will remain null. This is important because inside the finally block, Close( ) is called only if fin is not null. This mechanism prevents an attempt to call Close( ) on fin when it does not refer to an open file. Because of its compactness, this approach is used by many of the I/O examples in this book. Be aware, however, that it will not be appropriate in cases in which you want to deal separately with a failure to open a file, such as might occur if a user mistypes a filename. In such a situation, you might want to prompt for the correct name, for example, before entering a try block that accesses the file.
In general, precisely how you manage the opening, accessing, and closing of a file will be determined by your specific application. What works well in one case may not be appropriate for another. Thus, you must tailor this process to best fit the exact needs of your program.
To write a byte to a file, use the WriteByte( ) method. Its simplest form is shown here:
void WriteByte(byte value)
This method writes the byte specified by value to the file. If the underlying stream is not opened for output, a NotSupportedException is thrown. If the stream is closed, ObjectDisposedException is thrown.
You can write an array of bytes to a file by calling Write( ). It is shown here:
void Write(byte[ ] array, int offset, int count)
Write( ) writes count bytes from the array array, beginning at array [offset], to the file. If an error occurs during writing, an IOException is thrown. If the underlying stream is not opened for output, a NotSupportedException is thrown. Several other exceptions are also possible.
As you may know, when file output is performed, often that output is not immediately written to the actual physical device. Instead, output is buffered by the operating system until a sizable chunk of data can be written all at once. This improves the efficiency of the system. For example, disk files are organized by sectors, which might be anywhere from 128 bytes long, on up. Output is usually buffered until an entire sector can be written all at once. However, if you want to cause data to be written to the physical device whether the buffer is full or not, you can call Flush( ), shown here:
void Flush( )
An IOException is thrown on failure. If the stream is closed, ObjectDisposedException is thrown.
Once you are done with an output file, you must remember to close it. This can be done by calling Close( ). Doing so ensures that any output remaining in a disk buffer is actually written to the disk. Thus, there is no reason to call Flush( ) before closing a file.
Here is a simple example that writes to a file:
// Write to a file.
using System;
using System.IO;
class WriteToFile {
static void Main(string[] args) {
FileStream fout = null;
try {
// Open output file.
fout = new FileStream("test.txt", FileMode.CreateNew);
// Write the alphabet to the file.
for(char c = 'A'; c <= 'Z'; c++)
fout.WriteByte((byte) c);
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
if(fout != null) fout.Close();
}
}
}
The program first creates a file called test.txt for output by using FileMode.CreateNew. This means that the file must not already exist. (If it does exist, an IOException will be thrown.) After the file is open, the uppercase alphabet is written to the file. Once this program executes, test.txt will contain the following output:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
One advantage to the byte-oriented I/O used by FileStream is that you can use it on any type of file—not just those that contain text. For example, the following program copies any type of file, including executable files. The names of the source and destination files are specified on the command line.
/* Copy a file one byte at a time.
To use this program, specify the name of the source
file and the destination file. For example, to copy a
file called FIRST.DAT to a file called SECOND.DAT, use
the following command line:
CopyFile FIRST.DAT SECOND.DAT
*/
using System;
using System.IO;
class CopyFile {
static void Main(string[] args) {
int i;
FileStream fin = null;
FileStream fout = null;
if(args.Length != 2) {
Console.WriteLine("Usage: CopyFile From To");
return;
}
try {
// Open the files.
fin = new FileStream(args[0], FileMode.Open);
fout = new FileStream(args[1], FileMode.Create);
// Copy the file.
do {
i = fin.ReadByte();
if(i != -1) fout.WriteByte((byte)i);
} while(i != -1);
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
if(fin != null) fin.Close();
if(fout != null) fout.Close();
}
}
}
Although byte-oriented file handling is quite common, it is possible to use character-based streams for this purpose. The advantage to the character streams is that they operate directly on Unicode characters. Thus, if you want to store Unicode text, the character streams are certainly your best option. In general, to perform character-based file operations, you will wrap a FileStream inside either a StreamReader or a StreamWriter. These classes automatically convert a byte stream into a character stream, and vice versa.
Remember, at the operating system level, a file consists of a set of bytes. Using a StreamReader or StreamWriter does not alter this fact.
StreamWriter is derived from TextWriter. StreamReader is derived from TextReader. Thus, StreamWriter and StreamReader have access to the methods and properties defined by their base classes.
To create a character-based output stream, wrap a Stream object (such as a FileStream) inside a StreamWriter. StreamWriter defines several constructors. One of its most popular is shown here:
StreamWriter(Stream stream)
Here, stream is the name of an open stream. This constructor throws an ArgumentException if stream is not opened for output and an ArgumentNullException if stream is null. Once created, a StreamWriter automatically handles the conversion of characters to bytes. When you are done with the StreamWriter, you must close it. Closing the StreamWriter also closes the underlying stream.
Here is a simple key-to-disk utility that reads lines of text entered at the keyboard and writes them to a file called test.txt. Text is read until the user enters the word “stop”. It uses a FileStream wrapped in a StreamWriter to output to the file.
// A simple key-to-disk utility that demonstrates a StreamWriter.
using System;
using System.IO;
class KtoD {
static void Main() {
string str;
FileStream fout;
// First, open the file stream.
try {
fout = new FileStream("test.txt", FileMode.Create);
}
catch(IOException exc) {
Console.WriteLine("Error Opening File:\n" + exc.Message);
return ;
}
// Wrap the file stream in a StreamWriter.
StreamWriter fstr_out = new StreamWriter(fout);
try {
Console.WriteLine("Enter text ('stop' to quit).");
do {
Console.Write(": ");
str = Console.ReadLine();
if(str != "stop") {
str = str + "\r\n"; // add newline
fstr_out.Write(str);
}
} while(str != "stop");
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
fstr_out.Close();
}
}
}
In some cases, it might be more convenient to open a file directly using StreamWriter. To do so, use one of these constructors:
StreamWriter(string path)
StreamWriter(string path, bool append)
Here, path specifies the name of the file to open, which can include a full path specifier. In the second form, if append is true, then output is appended to the end of an existing file. Otherwise, output overwrites the specified file. In both cases, if the file does not exist, it is created. Also, both throw an IOException if an I/O error occurs. Other exceptions are also possible.
Here is the key-to-disk program rewritten so it uses StreamWriter to open the output file:
// Open a file using StreamWriter.
using System;
using System.IO;
class KtoD {
static void Main() {
string str;
StreamWriter fstr_out = null;
try {
// Open the file, wrapped in a StreamWriter.
fstr_out = new StreamWriter("test.txt");
Console.WriteLine("Enter text ('stop' to quit).");
do {
Console.Write(": ");
str = Console.ReadLine();
if(str != "stop") {
str = str + "\r\n"; // add newline
fstr_out.Write(str);
}
} while(str != "stop");
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
if(fstr_out != null) fstr_out.Close();
}
}
}
To create a character-based input stream, wrap a byte stream inside a StreamReader. StreamReader defines several constructors. A frequently used one is shown here:
StreamReader(Stream stream)
Here, stream is the name of an open stream. This constructor throws an ArgumentNullException if stream is null. It throws an ArgumentException if stream is not opened for input. Once created, a StreamReader will automatically handle the conversion of bytes to characters. When you are done with the StreamReader, you must close it. Closing the StreamReader also closes the underlying stream.
The following program creates a simple disk-to-screen utility that reads a text file called test.txt and displays its contents on the screen. Thus, it is the complement of the key-to-disk utility shown in the previous section:
// A simple disk-to-screen utility that demonstrates a StreamReader.
using System;
using System.IO;
class DtoS {
static void Main() {
FileStream fin;
string s;
try {
fin = new FileStream("test.txt", FileMode.Open);
}
catch(IOException exc) {
Console.WriteLine("Error Opening file:\n" + exc.Message);
return ;
}
StreamReader fstr_in = new StreamReader(fin);
try {
while((s = fstr_in.ReadLine()) != null) {
Console.WriteLine(s);
}
} catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
fstr_in.Close();
}
}
}
In the program, notice how the end of the file is determined. When the reference returned by ReadLine( ) is null, the end of the file has been reached. Although this approach works, StreamReader provides an alternative means of detecting the end of the stream: the EndOfStream property. This read-only property is true when the end of the stream has been reached and false otherwise. Therefore, you can use EndOfStream to watch for the end of a file. For example, here is another way to write the while loop that reads the file:
while(!fstr_in.EndOfStream) {
s = fstr_in.ReadLine();
Console.WriteLine(s);
}
In this case, the use of EndOfStream makes the code a bit easier to understand but does not change the overall structure of the sequence. There are times, however, when the use of EndOfStream can simplify an otherwise tricky situation, adding clarity and improving structure.
As with StreamWriter, in some cases, you might find it easier to open a file directly using StreamReader. To do so, use this constructor:
StreamReader(string path)
Here, path specifies the name of the file to open, which can include a full path specifier. The file must exist. If it doesn’t, a FileNotFoundException is thrown. If path is null, then an ArgumentNullException is thrown. If path is an empty string, ArgumentException is thrown. IOException and DirectoryNotFoundException are also possible.
As mentioned earlier, the standard streams, such as Console.In, can be redirected. By far, the most common redirection is to a file. When a standard stream is redirected, input and/or output is automatically directed to the new stream, bypassing the default devices. By redirecting the standard streams, your program can read commands from a disk file, create log files, or even read input from a network connection.
Redirection of the standard streams can be accomplished in two ways. First, when you execute a program on the command line, you can use the < and > operators to redirect Console.In and/or Console.Out, respectively. For example, given this program:
using System;
class Test {
static void Main() {
Console.WriteLine("This is a test.");
}
}
executing the program like this:
Test > log
will cause the line “This is a test.” to be written to a file called log. Input can be redirected in the same way. The thing to remember when input is redirected is that you must make sure that what you specify as an input source contains sufficient input to satisfy the demands of the program. If it doesn’t, the program will hang.
The < and > command-line redirection operators are not part of C#, but are provided by the operating system. Thus, if your environment supports I/O redirection (as is the case with Windows), you can redirect standard input and standard output without making any changes to your program. However, there is a second way that you can redirect the standard streams that is under program control. To do so, you will use the SetIn( ), SetOut( ), and SetError( ) methods, shown here, which are members of Console:
static void SetIn(TextReader newIn)
static void SetOut(TextWriter newOut)
static void SetError(TextWriter newError)
Thus, to redirect input, call SetIn( ), specifying the desired stream. You can use any input stream as long as it is derived from TextReader. To redirect output, call SetOut( ), specifying the desired output stream, which must be derived from TextWriter. For example, to redirect output to a file, specify a FileStream that is wrapped in a StreamWriter. The following program shows an example:
// Redirect Console.Out.
using System;
using System.IO;
class Redirect {
static void Main() {
StreamWriter log_out = null;
try {
log_out = new StreamWriter("logfile.txt");
// Redirect standard out to logfile.txt.
Console.SetOut(log_out);
Console.WriteLine("This is the start of the log file.");
for(int i=0; i<10; i++) Console.WriteLine(i);
Console.WriteLine("This is the end of the log file.");
} catch(IOException exc) {
Console.WriteLine("I/O Error\n" + exc.Message);
} finally {
if(log_out != null) log_out.Close();
}
}
}
When you run this program, you won’t see any of the output on the screen, but the file logfile.txt will contain the following:
This is the start of the log file.
0
1
2
3
4
5
6
7
8
9
This is the end of the log file.
On your own, you might want to experiment with redirecting the other built-in streams.
So far, we have just been reading and writing bytes or characters, but it is possible—indeed, common—to read and write other types of data. For example, you might want to create a file that contains ints, doubles, or shorts. To read and write binary values of the C# built-in types, you will use BinaryReader and BinaryWriter. When using these streams, it is important to understand that this data is read and written using its internal, binary format, not its human-readable text form.
A BinaryWriter is a wrapper around a byte stream that manages the writing of binary data. Its most commonly used constructor is shown here:
BinaryWriter(Stream output)
Here, output is the stream to which data is written. To write output to a file, you can use the object created by FileStream for this parameter. If output is null, then an ArgumentNullException is thrown. If output has not been opened for writing, ArgumentException is thrown. When you are done using a BinaryWriter, you must close it. Closing a BinaryWriter also closes the underlying stream.
BinaryWriter defines methods that can write all of C#’s built-in types. Several are shown in Table 14-5. All can throw an IOException. (Other exceptions are also possible.) Notice that a string is written using its internal format, which includes a length specifier. BinaryWriter also defines the standard Close( ) and Flush( ) methods, which work as described earlier.
A BinaryReader is a wrapper around a byte stream that handles the reading of binary data. Its most commonly used constructor is shown here:
BinaryReader(Stream input)
Here, input is the stream from which data is read. To read from a file, you can use the object created by FileStream for this parameter. If input has not been opened for reading or is otherwise invalid, ArgumentException is thrown. When you are done with a BinaryReader you must close it. Closing a BinaryReader also closes the underlying stream.
BinaryReader provides methods for reading all of C#’s simple types. Several commonly used methods are shown in Table 14-6. Notice that ReadString( ) reads a string that is stored using its internal format, which includes a length specifier. These methods throw an IOException if an error occurs. (Other exceptions are also possible.)
BinaryReader also defines three versions of Read( ), which are shown here:
These methods will throw an IOException on failure. Other exceptions are possible. Also defined is the standard Close( ) method.
Here is a program that demonstrates BinaryReader and BinaryWriter. It writes and then reads back various types of data to and from a file.
// Write and then read back binary data.
using System;
using System.IO;
class RWData {
static void Main() {
BinaryWriter dataOut;
BinaryReader dataIn;
int i = 10;
double d = 1023.56;
bool b = true;
string str = "This is a test";
// Open the file for output.
try {
dataOut = new
BinaryWriter(new FileStream("testdata", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("Error Opening File:\n" + exc.Message);
return;
}
// Write data to a file.
try {
Console.WriteLine("Writing " + i);
dataOut.Write(i);
Console.WriteLine("Writing " + d);
dataOut.Write(d);
Console.WriteLine("Writing " + b);
dataOut.Write(b);
Console.WriteLine("Writing " + 12.2 * 7.4);
dataOut.Write(12.2 * 7.4);
Console.WriteLine("Writing " + str);
dataOut.Write(str);
}
catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine();
// Now, read the data.
try {
dataIn = new
BinaryReader(new FileStream("testdata", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("Error Opening File:\n" + exc.Message);
return;
}
try {
i = dataIn.ReadInt32();
Console.WriteLine("Reading " + i);
d = dataIn.ReadDouble();
Console.WriteLine("Reading " + d);
b = dataIn.ReadBoolean();
Console.WriteLine("Reading " + b);
d = dataIn.ReadDouble();
Console.WriteLine("Reading " + d);
str = dataIn.ReadString();
Console.WriteLine("Reading " + str);
}
catch(IOException exc) {
Console.WriteLine("I/O Error:\n" + exc.Message);
} finally {
dataIn.Close();
}
}
}
The output from the program is shown here:
Writing 10
Writing 1023.56
Writing True
Writing 90.28
Writing This is a test
Reading 10
Reading 1023.56
Reading True
Reading 90.28
Reading This is a test
If you examine the testdata file produced by this program, you will find that it contains binary data, not human-readable text.
Here is a more practical example that shows how powerful binary I/O is. The following program implements a very simple inventory program. For each item in the inventory, the program stores the item’s name, the number on hand, and its cost. Next, the program prompts the user for the name of an item. It then searches the database. If the item is found, the inventory information is displayed.
/* Use BinaryReader and BinaryWriter to implement
a simple inventory program. */
using System;
using System.IO;
class Inventory {
static void Main() {
BinaryWriter dataOut;
BinaryReader dataIn;
string item; // name of item
int onhand; // number on hand
double cost; // cost
try {
dataOut = new
BinaryWriter(new FileStream("inventory.dat", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("Cannot Open Inventory File For Output");
Console.WriteLine("Reason: " + exc.Message);
return;
}
// Write some inventory data to the file.
try {
dataOut.Write("Hammers");
dataOut.Write(10);
dataOut.Write(3.95);
dataOut.Write("Screwdrivers");
dataOut.Write(18);
dataOut.Write(1.50);
dataOut.Write("Pliers");
dataOut.Write(5);
dataOut.Write(4.95);
dataOut.Write("Saws");
dataOut.Write(8);
dataOut.Write(8.95);
}
catch(IOException exc) {
Console.WriteLine("Error Writing Inventory File");
Console.WriteLine("Reason: " + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine();
// Now, open inventory file for reading.
try {
dataIn = new
BinaryReader(new FileStream("inventory.dat", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("Cannot Open Inventory File For Input");
Console.WriteLine("Reason: " + exc.Message);
return;
}
// Look up item entered by user.
Console.Write("Enter item to look up: ");
string what = Console.ReadLine();
Console.WriteLine();
try {
for(;;) {
// Read an inventory entry.
item = dataIn.ReadString();
onhand = dataIn.ReadInt32();
cost = dataIn.ReadDouble();
// See if the item matches the one requested.
// If so, display information.
if(item.Equals(what, StringComparison.OrdinalIgnoreCase)) {
Console.WriteLine(onhand + " " + item + " on hand. " +
"Cost: {0:C} each", cost);
Console.WriteLine("Total value of {0}: {1:C}." ,
item, cost * onhand);
break;
}
}
}
catch(EndOfStreamException) {
Console.WriteLine("Item not found.");
}
catch(IOException exc) {
Console.WriteLine("Error Reading Inventory File");
Console.WriteLine("Reason: " + exc.Message);
} finally {
dataIn.Close();
}
}
}
Here is a sample run:
Enter item to look up: Screwdrivers
18 Screwdrivers on hand. Cost: $1.50 each
Total value of Screwdrivers: $27.00.
In the program, notice how inventory information is stored in its binary format. Thus, the number of items on hand and the cost are stored using their binary format rather than their human-readable text-based equivalents. This makes it is possible to perform computations on the numeric data without having to convert it from its human-readable form.
There is one other point of interest in the inventory program. Notice how the end of the file is detected. Since the binary input methods throw an EndOfStreamException when the end of the stream is reached, the program simply reads the file until either it finds the desired item or this exception is generated. Thus, no special mechanism is needed to detect the end of the file.
Up to this point, we have been using sequential files, which are files that are accessed in a strictly linear fashion, one byte after another. However, you can also access the contents of a file in random order. One way to do this is to use the Seek( ) method defined by FileStream. This method allows you to set the file position indicator (also called the file pointer or simply the current position) to any point within a file.
The method Seek( ) is shown here:
long Seek(long offset, SeekOrigin origin)
Here, offset specifies the new position, in bytes, of the file pointer from the location specified by origin. The origin will be one of these values, which are defined by the SeekOrigin enumeration:
After a call to Seek( ), the next read or write operation will occur at the new file position. The new position is returned. If an error occurs while seeking, an IOException is thrown. If the underlying stream does not support position requests, a NotSupportedException is thrown. Other exceptions are possible.
Here is an example that demonstrates random access I/O. It writes the uppercase alphabet to a file and then reads it back in non-sequential order.
// Demonstrate random access.
using System;
using System.IO;
class RandomAccessDemo {
static void Main() {
FileStream f = null;
char ch;
try {
f = new FileStream("random.dat", FileMode.Create);
// Write the alphabet.
for(int i=0; i < 26; i++)
f.WriteByte((byte)('A'+i));
// Now, read back specific values.
f.Seek(0, SeekOrigin.Begin); // seek to first byte
ch = (char) f.ReadByte();
Console.WriteLine("First value is " + ch);
f.Seek(1, SeekOrigin.Begin); // seek to second byte
ch = (char) f.ReadByte();
Console.WriteLine("Second value is " + ch);
f.Seek(4, SeekOrigin.Begin); // seek to 5th byte
ch = (char) f.ReadByte();
Console.WriteLine("Fifth value is " + ch);
Console.WriteLine();
// Now, read every other value.
Console.WriteLine("Here is every other value: ");
for(int i=0; i < 26; i += 2) {
f.Seek(i, SeekOrigin.Begin); // seek to ith character
ch = (char) f.ReadByte();
Console.Write(ch + " ");
}
}
catch(IOException exc) {
Console.WriteLine("I/O Error\n" + exc.Message);
} finally {
if(f != null) f.Close();
}
Console.WriteLine();
}
}
The output from the program is shown here:
First value is A
Second value is B
Fifth value is E
Here is every other value:
A C E G I K M O Qs U W Y
Although Seek( ) offers the greatest flexibility, there is another way to set the current file position. You can use the Position property. As shown previously in Table 14-2, Position is a read/write property. Therefore, you can use it to obtain the current position, or to set the current position. For example, here is the code sequence from the preceding program that reads every other letter from the file, rewritten to use the Position property:
Console.WriteLine("Here is every other value: ");
for(int i=0; i < 26; i += 2) {
f.Position = i; // seek to ith character via Position
ch = (char) f.ReadByte();
Console.Write(ch + " ");
}
Sometimes it is useful to read input from or write output to an array. To do this, you will use MemoryStream. MemoryStream is an implementation of Stream that uses an array of bytes for input and/or output. MemoryStream defines several constructors. Here is the one we will use:
MemoryStream(byte[ ] buffer)
Here, buffer is an array of bytes that will be used for the source and/or target of I/O requests. The stream created by this constructor can be written to, read from, and supports Seek( ). When using this constructor, you must remember to make buffer large enough to hold whatever output you will be directing to it.
Here is a program that demonstrates the use of MemoryStream:
// Demonstrate MemoryStream.
using System;
using System.IO;
class MemStrDemo {
static void Main() {
byte[] storage = new byte[255];
// Create a memory-based stream.
MemoryStream memstrm = new MemoryStream(storage);
// Wrap memstrm in a reader and a writer.
StreamWriter memwtr = new StreamWriter(memstrm);
StreamReader memrdr = new StreamReader(memstrm);
try {
// Write to storage, through memwtr.
for(int i=0; i <10; i++)
memwtr.WriteLine("byte [" + i + "]: " + i);
// Put a period at the end.
memwtr.WriteLine(".");
memwtr.Flush();
Console.WriteLine("Reading from storage directly: ");
// Display contents of storage directly.
foreach(char ch in storage) {
if (ch == '.') break;
Console.Write(ch);
}
Console.WriteLine("\nReading through memrdr: ");
// Read from memstrm using the stream reader.
memstrm.Seek(0, SeekOrigin.Begin); // reset file pointer
string str = memrdr.ReadLine();
while(str != null) {
str = memrdr.ReadLine();
if(str[0] == '.') break;
Console.WriteLine(str);
}
} catch(IOException exc) {
Console.WriteLine("I/O Error\n" + exc.Message);
} finally {
// Release reader and writer resources.
memwtr.Close();
memrdr.Close();
}
}
}
The output from the program is shown here:
Reading from storage directly:
byte [0]: 0
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4]: 4
byte [5]: 5
byte [6]: 6
byte [7]: 7
byte [8]: 8
byte [9]: 9
Reading through memrdr:
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4]: 4
byte [5]: 5
byte [6]: 6
byte [7]: 7
byte [8]: 8
byte [9]: 9
In the program, an array of bytes called storage is created. This array is then used as the underlying storage for a MemoryStream called memstrm. From memstrm are created a StreamReader called memrdr and a StreamWriter called memwtr. Using memwtr, output is written to the memory-based stream. Notice that after the output has been written, Flush( ) is called on memwtr. Calling Flush( ) is necessary to ensure that the contents of memwtr’s buffer are actually written to the underlying array. Next, the contents of the underlying byte array are displayed manually, using a foreach loop. Then, using Seek( ), the file pointer is reset to the start of the stream, and the memory stream is read using memrdr.
Memory-based streams are quite useful in programming. For example, you can construct complicated output in advance, storing it in the array until it is needed. This technique is especially useful when programming for a GUI environment, such as Windows. You can also redirect a standard stream to read from an array. This might be useful for feeding test information into a program, for example.
For some applications, it might be easier to use a string rather than a byte array for the underlying storage when performing memory-based I/O operations. When this is the case, use StringReader and StringWriter. StringReader inherits TextReader, and StringWriter inherits TextWriter. Thus, these streams have access to methods defined by those two classes. For example, you can call ReadLine( ) on a StringReader and WriteLine( ) on a StringWriter.
The constructor for StringReader is shown here:
StringReader(string s)
Here, s is the string that will be read from.
StringWriter defines several constructors. The one we will use here is this:
StringWriter( )
This constructor creates a writer that will put its output into a string. This string (in the form of a StringBuilder) is automatically created by StringWriter. You can obtain the contents of this string by calling ToString( ).
Here is an example that uses StringReader and StringWriter:
// Demonstrate StringReader and StringWriter.
using System;
using System.IO;
class StrRdrWtrDemo {
static void Main() {
StringWriter strwtr = null;
StringReader strrdr = null;
try {
// Create a StringWriter.
strwtr = new StringWriter();
// Write to StringWriter.
for(int i=0; i <10; i++)
strwtr.WriteLine("This is i: " + i);
// Create a StringReader.
strrdr = new StringReader(strwtr.ToString());
// Now, read from StringReader.
string str = strrdr.ReadLine();
while(str != null) {
str = strrdr.ReadLine();
Console.WriteLine(str);
}
} catch(IOException exc) {
Console.WriteLine("I/O Error\n" + exc.Message);
} finally {
// Release reader and writer resources.
if(strrdr != null) strrdr.Close();
if(strwtr != null) strwtr.Close();
}
}
}
The output is shown here:
This is i: 1
This is i: 2
This is i: 3
This is i: 4
This is i: 5
This is i: 6
This is i: 7
This is i: 8
This is i: 9
The program first creates a StringWriter called strwtr and outputs to it using WriteLine( ). Next, it creates a StringReader using the string contained in strwtr. This string is obtained by calling ToString( ) on strwtr. Finally, the contents of this string are read using ReadLine( ).
The.NET Framework defines a class called File that you will find useful when working with files because it contains several static methods that perform common file operations. For example, File contains methods that copy or move a file, encrypt or decrypt a file, and delete a file. It provides methods that obtain or set information about a file, such as whether it exists, its time of creation, its last access time, and its attributes (such as whether it is read-only, hidden, and so on). File also includes some convenience methods that let you read from a file and write to a file. In addition, you can use a File method to open a file and obtain a FileStream reference to it. Although File contains far too many methods to examine each one, we will look at three. The first is Copy( ) and the other two are Exists( ) and GetLastAccessTime( ). These methods will give you an idea of the convenience that the File methods offer. File is definitely a class that you will want to explore.
NOTE Another class that defines several file-related methods is FileInfo. It differs from File in one important way: it provides instance methods and properties rather than static methods to perform file operations. Therefore, if you will be performing several file operations on the same file, then FileInfo might offer a more efficient solution.
Earlier in this chapter you saw a program that copied a file by manually reading bytes from one file and writing them to another. Although such a task is not particularly difficult, it can be fully automated by using the Copy( ) method defined by File. It has the two versions shown here:
static void Copy(string sourceFileName, string destFileName)
static void Copy(string sourceFileName, string destFileName, boolean overwrite)
Copy( ) copies the file specified by sourceFileName to the file specified by destFileName. The first version copies the file only if destFileName does not already exist. In the second form, if overwrite is passed true, the copy will overwrite the destination file if it exists. Both can throw several types of exceptions, including IOException and FileNotFoundException.
The following program uses Copy( ) to copy a file. Both the source and destination filenames are specified on the command line. Notice how much shorter this version is than the copy program shown earlier. It’s also more efficient.
/* Copy a file using File.Copy().
To use this program, specify the name of the source
file and the destination file. For example, to copy a
file called FIRST.DAT to a file called SECOND.DAT, use
the following command line:
CopyFile FIRST.DAT SECOND.DAT
*/
using System;
using System.IO;
class CopyFile {
static void Main(string[] args) {
if(args.Length != 2) {
Console.WriteLine("Usage: CopyFile From To");
return;
}
// Copy the files.
try {
File.Copy(args[0], args[1]);
} catch(IOException exc) {
Console.WriteLine("Error Copying File\n" + exc.Message);
}
}
}
As you can see, this version of the program does not require that you create a FileStream or release resources. The Copy( ) method handles all this for you. Also notice that this version will not overwrite an existing file. You might want to try using the second version of Copy( ), which does allow the destination to be overwritten.
Using File methods, it is quite easy to obtain information about a file. We will look at two such methods: Exists( ) and GetLastAccessTime( ). The Exists( ) method determines if a file exists. GetLastAccessTime( ) returns the date and time at which a file was last accessed. These two methods are shown here:
static bool Exists(string path)
static DateTime GetLastAccessTime(string path)
For both methods, path specifies the file about which to obtain information. Exists( ) returns true if the file exists and can be accessed by the calling process. GetLastAccessTime( ) returns a DateTime structure that contains the date and time the file was last accessed. (DateTime is described later in this book, but its ToString( ) method automatically creates a human-readable form of the date and time.) A number of exceptions relating to the use of an invalid argument or an invalid permission are possible. However, it will not throw an IOException.
The following program shows Exists( ) and GetLastAccessTime( ) in action. It determines if a file called test.txt exists. If it does, the time it was last accessed is displayed.
// Use Exists() and GetLastAccessTime().
using System;
using System.IO;
class ExistsDemo {
static void Main() {
if(File.Exists("test.txt"))
Console.WriteLine("File exists. It was last accessed at " +
File.GetLastAccessTime("test.txt"));
else
Console.WriteLine("Does Not Exist");
}
}
Sample output is shown here:
File exists. It was last accessed at 11/1/2009 5:30:17 PM
You can also obtain a file’s time of creation by calling GetCreationTime( ) and the time at which it was last written to by calling GetLastWriteTime( ). UTC versions of these methods are also available. (UTC stands for coordinated universal time.) You might want to experiment with these.
Before leaving the topic of I/O, we will examine a technique useful when reading numeric strings. As you know, WriteLine( ) provides a convenient way to output various types of data to the console, including numeric values of the built-in types, such as int and double. Thus, WriteLine( ) automatically converts numeric values into their human-readable form. However, a parallel input method that reads and converts strings containing numeric values into their internal, binary format is not provided. For example, there is no version of Read( ) that reads from the keyboard a string such as “100” and then automatically converts it into its corresponding binary value that can be stored in an int variable. Instead, there are other ways to accomplish this task. Perhaps the easiest is to use a method that is defined for all of the built-in numeric types: Parse( ).
Before we begin, it is necessary to state an important fact: all of C#’s built-in types, such as int and double, are actually just aliases (that is, other names) for structures defined by the.NET Framework. In fact, the C# type and.NET structure type are indistinguishable. One is just another name for the other. Because C#’s built-in types are supported by structures, they have members defined for them.
For the numeric types, the.NET structure names and their C# keyword equivalents are given here:
These structures are defined inside the System namespace. Thus, the fully qualified name for Int32 is System.Int32. These structures offer a wide array of methods that help fully integrate the built-in numeric types into C#’s object hierarchy. As a side benefit, the numeric structures also define a static method called Parse( ) that converts a numeric string into its corresponding binary equivalent.
There are several overloaded forms of Parse( ). The simplest version for each numeric structure is shown here. It performs the conversion using the default locale and numeric style. (Other versions let you perform locale-specific conversions and specify the numeric style.) Notice that each method returns a binary value that corresponds to the string.
The Parse( ) methods will throw a FormatException ifs does not contain a valid number as defined by the invoking type. ArgumentNullException is thrown ifs is null, and OverflowException is thrown if the value ins exceeds the bounds of the invoking type.
The parsing methods give you an easy way to convert a numeric value, read as a string from the keyboard or a text file, into its proper internal format. For example, the following program averages a list of numbers entered by the user. It first asks the user for the number of values to be averaged. It then reads that number using ReadLine( ) and uses Int32.Parse( ) to convert the string into an integer. Next, it inputs the values, using Double.Parse( ) to convert the strings into their double equivalents.
// This program averages a list of numbers entered by the user.
using System;
using System.IO;
class AvgNums {
static void Main() {
string str;
int n;
double sum = 0.0;
double avg, t;
Console.Write("How many numbers will you enter: ");
str = Console.ReadLine();
try {
n = Int32.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
return;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message);
return;
}
Console.WriteLine("Enter " + n + " values.");
for(int i=0; i < n ; i++) {
Console.Write(": ");
str = Console.ReadLine();
try {
t = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
t = 0.0;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message);
t = 0;
}
sum += t;
}
avg = sum / n;
Console.WriteLine("Average is " + avg);
}
}
Here is a sample run:
How many numbers will you enter: 5
Enter 5 values.
: 1.1
: 2.2
: 3.3
: 4.4
: 5.5
Average is 3.3
One other point: You must use the right parsing method for the type of value you are trying to convert. For example, trying to use Int32.Parse( ) on a string that contains a floating-point value will not produce the desired result.
As explained, Parse( ) will throw an exception on failure. You can avoid generating an exception when converting numeric strings by using the TryParse( ) method, which is defined for all of the numeric structures. Here is an example. It shows one version of TryParse( ) as defined by Int32.
static bool TryParse(string s, out int result)
The numeric string is passed in s. The result is returned in result. It performs the conversion using the default locale and numeric style. (A second version of TryParse( ) is available that lets you specify the numeric style and locale.) If the conversion fails, such as when s does not contain a numeric string in the proper form, TryParse( ) returns false. Otherwise, it returns true. Therefore, you must check the return value to confirm that the conversion was successful.