In the last two articles, we talked about creating, moving and copying files and directories. Today we are going to discuss writing to and reading from files using the available classes in the namespace System.IO.
Contributed by Michael Youssef Rating: / 38 January 31, 2007
We have many classes in this namespace like Stream, FileStream, StreamReader and StreamWriter, along with other classes that are used to read from and write to files. Today we are going to discuss the difference between those classes and how we can use them. The best way to understand the concepts of this topic is by practice, so as usual you are going to see a lot of examples.
Do you know what a stream is? No? Okay, a stream is a general representation of bytes. We use a stream-based class to read bytes from sources like files, memory or even network locations. We also use it to write bytes to those sources. So we can look at a stream as a bridge between a source like a file and your program to transfer data in its byte format. The .NET Framework provides the FileStream class which is used for read/write operations (as we said, in byte format) between your program and a file. The .NET Framework also provides the MemoryStream class, which is used for read/write operations between your program and memory.
Note that FileStream and MemoryStream classes are derived from the Stream abstract base-class. The Stream class has methods like Read(), ReadByte(), Write() and WriteByte() which are overridden in derived classes (like the FileStream and MemoryStream classes) to provide an implementation that's specific to the source. This makes sense because the implementation of the method WriteByte() of the FileStream class differs from the implementation of MemoryStream.WriteByte(); the first one writes a byte to a file and the latter one writes a byte to the memory.
Let's begin with the FileStream class so you can understand what we are talking about here.
The FileStream class inherits the Stream class to provide byte reading/writing operations to and from a source file. The FileStream class provides synchronous reading/writing methods as well asynchronous reading/writing methods. This class also provides a seeking functionality; I'll explain more about that later. You can construct a FileStream object in many ways, so let's look at some of them.
FileStream fStream = File.OpenWrite("theFile.txt"); You can have a FileStream object using the method File.OpenWrite(), which is passed the name of the file as a string value, then returns a FileStream object with FileAccess.Write. Notice that this method opens the file if it already exists or creates the file if it doesn't exist. Also note that you can't read from this FileStream object because it has been opened for write operations only. If you tried to read from this file, an exception of type NotSupportedException will be thrown.
FileStream fStream = File.OpenRead("theFile.txt"); You can return a FileStream object using the method File.OpenRead() which is passed the name of the file as a string value. Notice that this method opens the file, and if the file does not exist, an exception of type FileNotFoundException is thrown. Also note that you can't write to this FileStream object because it has been opened for read operations only. If you tried to write to this file an exception of type NotSupportedException will be thrown.
The method FileInfo.OpenWrite() instance method returns a FileStream object with write access, and the method FileInfo.OpenRead() returns a FileStream object with read access.
Using one of the FileStream object constructors: FileStream fStream = new FileStream("theFile.txt",FileMode.OpenOrCreate, FileAccess.Write,FileShare.None);
This overload accepts the file name, FileMode enumeration value, FileAccess enumeration value and FileShare enumeration value. Let's take a look at the FileMode, FileAccess and FileShare enumerations that are used with FileStream objects.
The Enumerations
The following two tables list the enumerations that are used with FileStream objects to set file mode and file access. Let's begin with the FileMode enumeration.
FileMode Enumeration
Description
Create
FileMode.Create value creates a file and if the file already exists, it will be overwritten.
CreateNew
Creates a file and if the file already exists, an IOException is thrown.
Append
Opens the file and starts writing at the end of the file. If the file doesn't exist a file is created. Note that you can use FileMode.Append only with FileAccess.Write or an exception of type ArgumentException is thrown.
Open
Opens the file if it exists. If the file does not exist, an exception of type FileNotFoundException is thrown.
OpenOrCreate
Opens the file if it exists. If the file does not exist, the file will be created.
Truncate
Opens a file and truncates it to make it a size of zero bytes.
The FileAccess enumeration values
FileAccess Enumeration
Description
Read
Sets read access to the file.
Write
Sets write access to the file.
ReadWrite
Sets read/write access to the file.
The last enumeration is the FileShare enumeration, which provides values for how the file is shared with other streams by the running process or other process.
FileShare Enumeration
Description
None
Obtains an exclusive access to the file until it's closed, which means that other streams can't read from or write to the file until this stream is closed.
Read
Other streams (in the current process or in another one) can obtain read access to the file.
Write
Other streams (in the current process or in another one) can obtain write access to the file
ReadWrite
Other streams can obtain ReadWrite access to the file
Using the methods of a FileStream instance is very easy. In this section we illustrate how you can write to a file using Write() and WriteByte() methods. Let's take a look at the code
using System; using System.IO;
namespace MyStreams { class Class1 { public static void Main() { try { FileStream fStream = File.Open("aFile.txt", FileMode.OpenOrCreate); Console.WriteLine("The FileStream object on the aFile.txt:"); Console.WriteLine("Can Read? {0}", fStream.CanRead); Console.WriteLine("Can Write? {0}", fStream.CanWrite); Console.WriteLine("Can Seek? {0}", fStream.CanSeek); Console.WriteLine("Before we start writing bytes the current position: {0}",fStream.Position); if(fStream.CanWrite) { fStream.WriteByte(65); byte[] bytes = new byte[3] {66, 67, 68}; fStream.Write(bytes, 0, bytes.Length); Console.WriteLine("Bytes have been written to the file"); } Console.WriteLine("After we have written the bytes the current position: {0}", fStream.Position); fStream.Close(); Console.ReadLine(); } catch(IOException ex) { Console.WriteLine(ex.Message); } } } }
When you compile and run the above code you get the following screen shot:
Navigate to the application folder and open the text file.
Let's walk through the code step-by-step. The first line in the try block of the Main() method creates a FileStream instance as the return value of calling the method File.Open(), which is passed two values. The first value is a path string to the file and the second value is a FileAccess.OpenOrCreate enumeration value, which opens the file if it's already there or creates a new one. Note that this constructor establishes read/write access to the file.
The following three lines print out the value of the three properties CanRead, CanWrite and CanSeek respectively. You can use those properties to investigate the capabilities of the stream object in hand. For example, because our stream object (I mean the FileStream object) has been initialized using the constructor that gives read/write access to the file, we got true from CanRead and CanWrite. As to CanSeek: FileStream objects support seeking, so the property CanSeek always returns true -- but when you work with NetworkStream objects, CanSeek always returns false because NetworkStream objects don't support the concept of seeking.
The next line prints out the value of the property FileStream.position, which returns the current position in the stream. Right now it's zero because we have just created the file and haven't performed any operations yet. Next we check the value of the CanWrite property, which returns true as we just said. Because this stream can write, we use the FileStream.WriteByte() instance method to write a single byte to the stream. Note that this method throws an exception if the object doesn't support writing operations.
On the next line we create a byte array of three values (66, 67, 68) and write those bytes in one statement using the method FileStream.Write(), which accepts three parameters. The first parameter is the byte array that will be written to the stream, while the second parameter is the starting index of the first array, at which the writing begins. The third parameter defines how many bytes are written to the stream.
The first line after the if statement prints out the value of the property FileStream.Position which is four because we have written four bytes to the stream. The next line calls the instance method FileStream.Close() which clears the buffer, causing any buffered data to be written to the source file, then closes other resources obtained by this file.
In the following example we are going to read the bytes of the file we just created.
using System; using System.IO;
namespace MyStreams { class Class1 { public static void Main() { try { using(FileStream fStream = File.OpenRead("aFile.txt")) { Console.WriteLine("Investigating the file capabilities"); Console.WriteLine("Can Read? {0}", fStream.CanRead); Console.WriteLine("Can Write? {0}", fStream.CanWrite); Console.WriteLine("Can Seek? {0}", fStream.CanSeek); Console.WriteLine("Before we start reading bytes the current position: {0}", fStream.Position); if(fStream.CanRead) { // returns the bytes of the file byte[] bytes = new byte[fStream.Length]; fStream.Read(bytes, 0, bytes.Length); foreach(byte b in bytes) { Console.Write(" {0} ", b); } /* This code can be used in place of the above code * int temp = 0; while((temp = fStream.ReadByte()) != -1) { Console.WriteLine(temp); }*/
/* This code can be used in place of the above while statement * while(fStream.Position < fStream.Length) { Console.Write(" {0} ", fStream.ReadByte()); } * */ } Console.WriteLine("nAfter we have read the bytes the current position: {0}",fStream.Position); Console.ReadLine(); } } catch(IOException ex) { Console.WriteLine(ex.Message); } } } }
Compile and run the code to get the following screen shot.
As you can see, we have read the bytes of the file (65 66 67 68). Note that there are three ways to read the bytes of the file which you can try. The one I have used for this example is creating a byte array then using the FileStream.Read() method, which accepts three parameters. The array that is used to store the bytes reads from the file and the index of the array at which to begin to store the bytes, then the last parameter states how many bytes you want to read and store into the array. The next foreach statement prints out each byte value stored in the array. Note that I have used the instance property FileStream.Length as the length of the byte array that's used to store the bytes of the file. The Length property returns the size of the file in bytes.
The next commented solution uses a while loop. the FileStream.ReadByte() instance method reads a byte (but returns it as int value), advances the position of the stream by 1 and returns -1 if there are no bytes to return. So the while loop expression (temp = fStream.ReadByte()) != -1) prints out the byte as long as the value assigned by ReadByte() to temp is not -1.
The last solution also uses a while loop, but this time the while loop expression (fStream.Position < fStream.Length) evaluates to true as long as the position of the stream is less than the length of the stream. Note that we have used a using statement with the FileStream instance to ensure an auto call to the FileStream.Close() method.
The FileStream class provides the Seek() method which changes the position of the stream in a relative way to one of three values: in relation to the beginning of the stream (using the enumeration SeekOrigin.Begin value), in relation to the current position of the stream (SeekOrigin.Current) and in relation to the end of the stream (SeekOrigin.End). Let's look at an example that explains how you can use this method.
The idea of our example is to use the Seek() method to change the current position in the stream and then read the available bytes until we reach the end of the stream. As you know, our file contains the bytes 65 66 67 68. The first Seek method (Seek(1, SeekOrigin.Begin) moves the position of the stream by one relative to the beginning of the stream, causing the reading operation to read only the three remaining bytes. Note that we set the position of the stream using the FileStream.Position to zero before using the next Seek() method.
The second Seek() method moves the position by two relative to SeekOrigin.Current (currently equal to the first byte because we just assigned 0 to the Position property). This means that the read operation will begin at the third byte. The last Seek() method moves the position in the stream to -1 of the end of the stream (SeekOrigin.End), which means that it is reading only the last byte in the stream.