You need to create a file—possibly for logging information to or storing temporary information—and then write information to it. You also need to be able to read the information that you wrote to this file.
To create, write to, and read from a log file, we will use the
FileStream
and its reader and writer classes. For
example, we will create methods to allow construction, reading to,
and writing from a log file.
To create a log file, you can use
the following code:
public FileStream CreateLogFile(string logFileName) { FileStream fileStream = new FileStream(logFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.None); return (fileStream); }
To write text to this file, you can create
a StreamWriter
object wrapper around the
previously created FileStream
object
(fileStream
). You can then use the
WriteLine
method of
the StreamWriter
object. The following code writes
three lines to the file: a string, followed by an integer, followed
by a second string:
public void WriteToLog(FileStream fileStream, string data) { // make sure we can write to this stream if(!fileStream.CanWrite) { // close it and reopen for append string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Append); } StreamWriter streamWriter = new StreamWriter(fileStream); streamWriter.WriteLine(data); streamWriter.Close( ); }
Now
that the file has been created and data has been written to it, you
can read the data from this file. To read text from a file, create a
StreamReader
object
wrapper around the file. If the code had not closed the
FileStream
object (fileStream
),
it could use that object in place of the filename used to create the
StreamReader
. To read the entire file in as a
single string, use the ReadToEnd
method:
public string ReadAllLog(FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } StreamReader streamReader = new StreamReader(fileStream); string contents = streamReader.ReadToEnd( ); streamReader.Close( ); return contents; }
If you need to read the lines in one by one, use the
Peek
method, as
shown in ReadLogPeeking
, or the
ReadLine
method, as
shown in
ReadLogByLines
:
public static void ReadLogPeeking(FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } Console.WriteLine("Reading file stream peeking at next line:"); StreamReader streamReader = new StreamReader(fileStream); while (streamReader.Peek( ) != -1) { Console.WriteLine(streamReader.ReadLine( )); } streamReader.Close( ); }
public static void ReadLogByLines(FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } Console.WriteLine("Reading file stream as lines:"); StreamReader streamReader = new StreamReader(fileStream); string text = ""; while ((text = streamReader.ReadLine( )) != null) { Console.WriteLine(text); } streamReader.Close( ); }
If you
need to read in each character of the file as a byte value, use the
Read
method, which returns a
byte
value:
public static void ReadAllLogAsBytes(FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } Console.WriteLine("Reading file stream as bytes:"); StreamReader streamReader = new StreamReader(fileStream); while (streamReader.Peek( ) != -1) { Console.Write(streamReader.Read( )); } streamReader.Close( ); }
This method displays numeric byte values instead of the characters that they represent. For example, if the log file contained the following text:
This is the first line. 100 This is the third line.
it would be displayed by the ReadAllLogAsBytes
method, as follows:
841041051153210511532116104101321021051141151163210810 511010146131049484813108410410511532105115321161041013 211610410511410032108105110101461310
If you need to read in the file by chunks, create and fill a buffer of an arbitrary length based on your performance needs. This buffer can then be displayed or manipulated as needed:
public static void ReadAllBufferedLog (FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } Console.WriteLine("Reading file stream as buffers of bytes:"); StreamReader streamReader = new StreamReader(fileStream); while (streamReader.Peek( ) != -1) { char[] buffer = new char[10]; int bufferFillSize = streamReader.Read(buffer, 0, 10); foreach (char c in buffer) { Console.Write(c); } Console.WriteLine(bufferFillSize); } streamReader.Close( ); }
This method displays the log file’s characters in 10-character chunks, followed by the number of characters actually read. For example, if the log file contained the following text:
This is the first line. 100 This is the third line.
it would be displayed by the ReadAllBufferedLog
method as follows:
This is th10 e first li10 ne. 100 10 This is th10 e third li10 ne. 5
Notice that at the end of every tenth character (the buffer is a
char
array of size 10
), the
number of characters read in is displayed. During the last read
performed, only five characters were left to read from the file. In
this case, a 5
is displayed at the end of the
text, indicating that the buffer was not completely filled.
The previous code could have been modified to use the
ReadBlock
method as
shown in the ReadAllBufferedLogBlock
method
instead of the Read
method. The output is the same
in both cases:
public static void ReadAllBufferedLogBlock(FileStream fileStream) { if(!fileStream.CanRead) { // close it and reopen for read string fileName = fileStream.Name; fileStream.Close( ); fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read); } Console.WriteLine("Reading file stream as buffers of bytes using ReadBlock:"); StreamReader streamReader = new StreamReader(fileStream); while (streamReader.Peek( ) != -1) { char[] buffer = new char[10]; int bufferFillSize = streamReader.ReadBlock(buffer, 0, 10); foreach (char c in buffer) { Console.Write(c); } Console.WriteLine(bufferFillSize); } streamReader.Close( ); }
This displays the following text:
This is th10 e first li10 ne. 100 10 This is th10 e third li10 ne. 5
There are many mechanisms for recording state information about applications, other than creating a file full of the information. One example of this type of mechanism is the Windows Event Log, where informational, security, and error states can be logged during an application’s progress. One of the primary reasons for creating a log file is to assist in troubleshooting or to debug your code in the field. If you are shipping code without some sort of debugging mechanism for your support staff (or possibly for you in a small company), we suggest you consider adding some logging support. Any developer who has spent a late night debugging a problem on a QA machine, or worse yet, at a customer site, can tell you the value of a log of the program’s actions.
If you are writing character
information to a file, the simplest method is to use the
Write
and WriteLine
methods of
the StreamWriter
class to write data to the file.
These two methods are overloaded to handle any of the primitive
values (except for the byte
data type), as well as
character arrays. These methods are also overloaded to handle various
formatting techniques discussed in Chapter 1. All
of this information is written to the file as character text, not as
the underlying primitive type.
If you need to write
byte
data to a file, consider using the
Write
and WriteByte
methods of
the FileStream
class. These methods are designed
to write byte
values to a file. The
WriteByte
method accepts a single
byte
value and writes it to the file, after which
the pointer to the current position in the file is advanced to the
next value after this byte
. The
Write
method accepts an array of bytes that can be
written to the file, after which the pointer to the current position
in the file is advanced to the next value after this array of bytes.
The Write
method can also choose a range of bytes
in the array in which to write to the file.
The Write
method of the
BinaryWriter
class overloaded similarly to the
Write
method of the
StreamWriter
class. The main difference is that
the BinaryWriter
class’s
Write
method does not allow formatting. This
allows the BinaryReader
to read the information
written by the BinaryWriter
as its underlying
type, not as a character or a byte
. See Recipe 11.18 for an example of the
BinaryReader
and BinaryWriter
classes in action.
Once
we have the data written to the file, we can read it back out. The
first concern when reading data from a file is not to go past the end
of the file. The
StreamReader
class
provides a Peek
method that looks—but does
not retrieve—the next character in the file. If the end of the
file has been reached, a -1
is returned. Likewise,
the
Read
method of this
class also returns a -1
if it has reached the end
of the file. The Peek
and Read
methods can be used in the following manner to make sure that you do
not go past the end of the file:
StreamReader streamReader = new StreamReader("data.txt"); while (streamReader.Peek( ) != -1) { Console.WriteLine(streamReader.ReadLine( )); } streamReader.Close( );
or:
StreamReader streamReader = new StreamReader("data.txt"); string text = ""; while ((text = streamReader.Read( )) != -1) { Console.WriteLine(text); } streamReader.Close( );
The main differences between the Read
and
Peek
methods are that the Read
method actually retrieves the next character and increments the
pointer to the current position in the file by one character, and the
Read
method is overloaded to return an array of
characters instead of just one. If the Read
method
is used that returns an array buffer of characters and the buffer is
larger than the file, the extra elements in the buffer array are set
to an empty string.
The
StreamReader
also contains a method to read an
entire line up to and including the newline character. This method is
called ReadLine
. This method returns a
null
if it goes past the end of the file. The
ReadLine
method can be used in the following
manner to make sure that you do not go past the end of the file:
StreamReader streamReader = new StreamReader("data.txt"); string text = ""; while ((text = streamReader.ReadLine( )) != null) { Console.WriteLine(text); } streamReader.Close( );
If
you simply need to read the whole file in at one time, use the
ReadToEnd
method to read the entire file in to a
string. If the current position in the file is moved to a point other
than the beginning of the file, the ReadToEnd
method returns a string of characters starting at that position in
the file and ending at the end of the file.
The
FileStream
class contains two methods,
Read
and ReadByte
, which read
one or more bytes of the file. The Read
method
reads a byte
value from the file and casts that
byte
to an int
before returning
the value. If you are explicitly expecting a byte
value, consider casting the return type to a byte
value:
FileStream fileStream = new FileStream("data.txt", FileMode.Open); byte retVal = (byte) fileStream.ReadByte( );
However, if retVal
is being used to determine
whether the end of the file has been reached (i.e.,
retVal
==
-1
or retVal
==
0xffffffff
in hexadecimal), you will run into
problems. When the return value of ReadByte
is
cast to a byte
, a -1
is cast to
0xff
, which is not equal to -1
but is equal to 255
(the byte
data type is not signed). If you are going to cast this return type
to a byte
value, you cannot use this value to
determine whether you are at the end of the file. You must instead
rely on the Length
Property. The following code
block shows the use of the return value of the
ReadByte
method to determine when we are at the
end of the file:
FileStream fileStream = new FileStream("data.txt", FileMode.Open); int retByte = 0; while ((retByte = fileStream.ReadByte( )) != -1) { Console.WriteLine((byte)retByte); } fileStream.Close( );
This code block shows the use of the Length
property to determine when to stop reading the file:
FileStream fileStream = new FileStream("data.txt", FileMode.Open); long currPosition = 0; while (currPosition < fileStream.Length) { Console.WriteLine((byte) fileStream.ReadByte( )); currPosition++; } fileStream.Close( );
The
BinaryReader
class contains several methods for
reading specific primitive types, including character arrays and
byte
arrays. These methods can be used to read
specific data types from a file. Recipe 11.18 contains more on this topic. All of
these methods, except for the Read
method,
indicate that the end of the file has been reached by throwing the
EndOfStreamException
. The Read
method
will return a -1
if it is trying to read past the
end of the file. This class contains a PeekChar
method that is very similar to the Peek
method in
the StreamReader
class. The
PeekChar
method is used as follows:
FileStream fileStream = new FileStream("data.txt", FileMode.Open); BinaryReader binaryReader = new BinaryReader(fileStream); while (binaryReader.PeekChar( ) != -1) { Console.WriteLine(binaryReader.ReadChar( )); } binaryReader.Close( );
In this code, the PeekChar
method is used to
determine when to stop reading values in the file. This will prevent
a costly EndOfStreamException
from being thrown by
the ReadChar
method if it tries to read past the
end of the file.