Creating, Writing to, and Reading from a File






Creating, Writing to, and Reading from a File

Problem

You need to create a filepossibly for logging information to or for storing temporary informationand then write information to it. You also need to be able to read the information that you wrote to this file.

Solution

To create, write to, and read from a logfile, 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 logfile. To create a logfile, you can use the following code:

	FileStream fileStream = new FileStream(logFileName,
	                                   FileMode.Append,
	                                   FileAccess.Write,
	                                   FileShare.None);

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 static void WriteToLog(string logFileName, string data)
	{
	    using (FileStream fileStream = new FileStream(logFileName,
	            FileMode.Append,
	            FileAccess.Write,
	            FileShare.None))
	    { 
	        using (StreamWriter streamWriter = new StreamWriter(fileStream)) 
	        {
	            streamWriter.WriteLine(data);
	        }
	    }
	}

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 has not closed the FileStream object (fileStream), it can 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 static string ReadAllLog(string logFileName)
	{
	    if (!File.Exists(logFileName))
	    {
	        throw (new FileNotFoundException( 
	          "logfile cannot be read since it does not exist.", logFileName)); 
	    }
	    string contents = "";
	
	    using (FileStream fileStream = new FileStream(logFileName,
	                FileMode.Open,
	                FileAccess.Read,
	                FileShare.None))
	    { 
	        using (StreamReader streamReader = new StreamReader(fileStream)) 
	        {
	            contents = streamReader.ReadToEnd();
	        }
	    }
	
	    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, both of which appear in Figure.

ReadLogPeeking and ReadLogByLines methods

public static void ReadLogPeeking(string logFileName)
{ 
    if (!File.Exists(logFileName)) 
    {
        throw (new FileNotFoundException(
          "logfile cannot be read since it does not exist.", logFileName));
    }

    using (FileStream fileStream = new FileStream(logFileName,
                FileMode.Open,
                FileAccess.Read,
                FileShare.None))
    {
        Console.WriteLine("Reading file stream peeking at next line:");
        using (StreamReader streamReader = new StreamReader(fileStream))
        {
        while (streamReader.Peek() != -1) 
        { 
            Console.WriteLine(streamReader.ReadLine()); 
        } 
     } 
   } 
}

or:

	public static void ReadLogByLines(string logFileName)
	{
	    if (!File.Exists(logFileName))
	    {
	        throw (new FileNotFoundException( 
	          "Logfile cannot be read since it does not exist.", logFileName));    }

	using (FileStream fileStream = new FileStream(logFileName,
	            FileMode.Open,
	            FileAccess.Read,
	            FileShare.None))
	{ 
	   Console.WriteLine("Reading file stream as lines:"); 
	   using (StreamReader streamReader = new StreamReader(fileStream)) 
	   {
	       string text = streamReader.ReadLine();
	       while (text != null)
	       {
	          Console.WriteLine(text);
	          text = streamReader.ReadLine();
	       }
	    }
	}
}

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(string logFileName)
{
if (!File.Exists(logFileName))
{
throw (new FileNotFoundException( 
"Logfile cannot be read since it does not exist.", logFileName));    }

	using (FileStream fileStream = new FileStream(logFileName,
	            FileMode.Open,
	            FileAccess.Read,
	            FileShare.None))
	{
	   Console.WriteLine("Reading file stream as bytes:"); 
	   using (StreamReader streamReader = new StreamReader(fileStream)) 
	   {
	       while (streamReader.Peek() != -1)
           {
               Console.Write(streamReader.Read());
           }
        }
    }
}

This method displays numeric byte values instead of the characters that they represent. For example, if the logfile 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(string logFileName)
	{
	    if (!File.Exists(logFileName))
	    {
	        throw (new FileNotFoundException( 
	          "Logfile cannot be read since it does not exist.", logFileName)); 
	    }

	    using (FileStream fileStream = new FileStream(logFileName,
	                FileMode.Open,
	                FileAccess.Read,
	                FileShare.None))
	    { 
	        Console.WriteLine("Reading file stream as buffers of bytes:");             using
 (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);
	            }
	        }
	    }
	}

This method displays the logfile's characters in 10-character chunks, followed by the number of characters actually read. For example, if the logfile 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 10th 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.

Discussion

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 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 Recipe 2.16. 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 location 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 location after this array of bytes. The Write method can also choose a range of bytes in the array to write to the file.

The Write method of the BinaryWriter class is overloaded in a similar fashion to the Write method of the StreamWriter class. The main difference is that the BinaryWriter class's Write method does not allow formatting. This forces the BinaryReader to read the information written by the BinaryWriter as its underlying type, not as a character or a byte. See Recipe 12.5 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 to not go past the end of the file. The StreamReader class provides a Peek method that looksbut does not retrievethe 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:

	using (StreamReader streamReader = new StreamReader("data.txt"))
	{
	    while (streamReader.Peek( ) != -1)
	    {
	        Console.WriteLine(streamReader.ReadLine( ));
	    }
	}

or:

	using (StreamReader streamReader = new StreamReader("data.txt"))
	{
	    int nextChar = streamReader.Read();
	    while (nextChar > -1)
	    {
	        Console.WriteLine((char)nextChar);
	        nextChar = streamReader.Read();	
	    }
	}

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 you use the Read method that returns a buffer of characters and the buffer is larger than the file, the extra elements in the buffer array are untouched.

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:

	using (StreamReader streamReader = new StreamReader("data.txt"))
	{
	    string text = streamReader.ReadLine( );
	    while (text != null)
	    {
	        Console.WriteLine(text);
	        text = streamReader.ReadLine( );
	    }
	}

If you simply need to read the whole file in at one time, use the ReadToEnd method to read the entire file into 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:

	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:

	using (FileStream fileStream = new FileStream("data.txt", FileMode.Open))
	{
	    int retByte = fileStream.ReadByte( );
	    while (retByte != -1)
	    {
	        Console.WriteLine((byte)retByte);
	        retByte = fileStream.ReadByte( );
	    }
	}

This code block shows the use of the Length property to determine when to stop reading the file:

	using (FileStream fileStream = new FileStream("data.txt", FileMode.Open))
	{
	    long currPosition = 0;
	    while (currPosition < fileStream.Length)
	    {
	        Console.WriteLine((byte) fileStream.ReadByte( ));
	        currPosition++;
	    }
	}

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. 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:

	using (FileStream fileStream = new FileStream("data.txt", FileMode.Open))
	{
	    BinaryReader binaryReader = new BinaryReader(fileStream);
	    while (binaryReader.PeekChar( ) != -1)
	    {
	        Console.WriteLine(binaryReader.ReadChar( ));
	    }
	}

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.

See Also

See the "FileStream Class," "StreamReader Class," "StreamWriter Class," "BinaryReader Class," and "BinaryWriter Class" topics in the MSDN documentation.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows