File Input and Output

( 0 users )

The file and stream input/output form a core part of .Net framework library which is used to send data to and receive data from different storage mediums. It enables us to read and write to files on storage disks, memory and over networks. It also provides different formats to read or write data like in binary, text and other encodings. The .Net has combined all these features under one namespace and that is "System.IO". This namespace contains many classes and operations which seamlessly provides the interaction with data storage and retrieval.

Before taking a look at what all these classes and operations are, we need to first understand how the "System.IO" is designed. Every class and operations are operated on Streams. A Stream is a fundamental concept related to sending and receiving data. A Stream is a continuous array of bytes that acts as a medium of getting and retrieving information. A Stream is associated with different mediums like file, string, memory, etc and .Net framework provides different classes to operate of such medium of streams.

For all such stream operations, the .Net framework provides an abstract base class "Stream". All other medium-specific stream classes are inherited from this "Stream" class. The "Stream" class handles all low-level processing like accessing files using operating system shell and accessing storage devices and mediums.

The "Stream" class

The "Stream" is an abstract base class of all I/O operations. The underlying stream of information is bytes as it works on the byte data type. Be it reading or writing, everything is a byte stream at this level. The other stream classes inherit this base class and act as a wrapper over byte stream to provide a stream of another data type like char. However, behind the scenes, it is always the byte that carries stream information.

It has basically have three fundamental operations.

  1. Reading from streams
  2. Writing to streams
  3. Seeking within streams

Some of the important abstract methods provided by the "Stream" class provides are:

Method NameDescription
CloseThis is used to close the underlying stream and free any resources used.
ReadIt is used to read some bytes from the stream. At the same time, it moves the current position by the number of bytes already read.
WriteIt is used to write some bytes to the stream. Like "Read(..)", it also moves the current position by the number of bytes already read.
SeekIt is used to set the current position in the current stream without performing any other action.

Let's see some of the concrete Stream classes (derived from abstract base class "Stream").

The "FileStream" class

The "FileStream" class is the most frequently used Stream class as it is involved mostly in file reading and writing to disk. The "FileStream" class has several constructors with which we can create a file stream that is available for reading and/or writes operations. The following two are commonly used constructors.


FileStream(String, FileMode);
FileStream(String, FileMode, FileAccess);
		

Here the first parameter of string type takes a physical location of the file path as a string. For e.g. "c:/example.txt" in windows. The next parameter "FileMode" is an enum type that defines the way in which a file needs to be opened. Here are the different values of "FileMode" and their usage.

FileMode typeDescription
OpenIn this mode, an existing file will be opened. Trying to open a non-existing file will lead to a run time exception.
CreateIn this mode, a file will be created. If the file already exists then it will be overwritten.
OpenOrCreateIn this mode, if the file does not exists then it will be created, otherwise it will be opened.
AppendIn this mode, if the file does not exists in the provided path it will create a new file. Otherwise, the file will simply be opened in write mode and seeks the file to the end.

The third parameter is "FileAccess" which specifies three access modes in which we can open a file.

FileAccess typeDescription
ReadIn this mode, we can only read the data from FileStream.
WriteIn this mode, we can only write the data to the FileStream.
ReadWriteIn this mode, we can both read from the FileStream and write to the FileStream.

Reading a file with FileStream

Let's try to access and read a file present in the disk. For this, you need to create a new file called "example.txt" in your drive. For now, manually edit this file and write "Hello World" and save the file. To access and read this file programmatically in C#, we can write the following code.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("FileStream");
	Console.WriteLine();

    FileStream fs = new FileStream("example.txt", FileMode.Open);
    byte[] readBytes = new byte[128];

    fs.Read(readBytes, 0, readBytes.Length);

    fs.Close();

    Console.WriteLine(Encoding.ASCII.GetString(readBytes));

    Console.Write("Press any key to continue...");
	Console.ReadKey(true);
  }
}
			

In the above program, we have created an instance of FileStream by providing a file path and the FileMode as "Open". To read the contents from the file, we simply used its "Read(...)" method which takes a buffer as a byte array and it stores the contents in that array. We need to call the Close() method to release the file after we are all done using it. In the end, we are simply printing the contents from the byte array. The "Encoding" class provides us with various options to convert bytes array to string data type. The program will output "Hello World" to the console.

Important Note : Every time we are using the Streams, we need to call the Close() method to release our resources. This sometimes gives an additional overhead to make sure we don't miss it in our program. Luckily, there is an alternative to it. Every stream class implements the "IDisposable" interface. The Close() method internally does some stuff and calls the dispose method. In C#, however, we can use the "using" keyword to define a set of blocks. As soon as this block completes its execution the CLR automatically invokes the "Dispose" method of the object provided within the using construct. Let's convert the above example and use the "using" keyword so that we don't have to worry about calling the Close() method every time.


static void Main(string[] args)
{
	Console.WriteLine("FileStream");
	Console.WriteLine();

	byte[] readBytes = new byte[128];
	using(FileStream fs = new FileStream("example.txt", FileMode.Open))
	{
		fs.Read(readBytes, 0, readBytes.Length);
	}

	Console.WriteLine(Encoding.ASCII.GetString(readBytes));

	Console.Write("Press any key to continue...");
	Console.ReadKey(true);
}
			

Going forward we will use the "using" keyword for all our stream operations.

Writing into a file with FileStream

The "Write(...)" method of the FileStream object is used to write contents in a file. Let's see more in the below example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("FileStream");
		Console.WriteLine();

    var encoder = new UnicodeEncoding();

    byte[] writeBytes = encoder.GetBytes("FileStream write example.");
    using(FileStream fs = new FileStream("example.txt", FileMode.Create, FileAccess.Write))
    {
      fs.Write(writeBytes, 0, writeBytes.Length);
    }

    Console.WriteLine("Contents have been written. Check the file.");

    Console.Write("Press any key to continue...");
		Console.ReadKey(true);
  }
}
			

In the above example, first, we are using a text encoder that converts a string into a byte array. Notice that we have specified FileMode as "Create" and FIleAccess as "Write". Then, we are calling the Write(...) method of the FileStream object. After the program finishes execution, you can check out the file "example.txt" and verify the text written.

Next we will learn about "MemoryStream" class.

The "MemoryStream" class

The "MemoryStream" class is used to read from and write to the main memory. It is sometimes used as a temporary store before finally writing to a file using FileStream. We can initialize the "MemoryStream" with several constructors. Some of the commonly used constructors are:

MemoryStream constructorsDescription
MemoryStream()The default constructor. This has expandable capacity initialized to zero. [Note : The capacity of MemoryStream is the number of bytes allocated initially which grows as we provide more buffer to the stream. ]
MemoryStream(Byte[])It takes the source bytes of array from where to read the contents.
MemoryStream(Int32)It initializes the MemoryStream object with capacity initialized to the provided value in its argument.

Let's see an example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("MemoryStream");
	Console.WriteLine();

    var encoder = new UnicodeEncoding();

    byte[] sourceBuffer = encoder.GetBytes("MemoryStream example.");
    byte[] targetBuffer = new byte[sourceBuffer.Length];

    using(MemoryStream mStream = new MemoryStream(sourceBuffer))
    {
		mStream.Read(targetBuffer, 0, targetBuffer.Length);
		sourceBuffer = null;
    }

    var text = encoder.GetString(targetBuffer, 0, targetBuffer.Length);
    Console.WriteLine(text);

    Console.Write("Press any key to continue...");
	Console.ReadKey(true);
  }
}
		

In the above example, we have provided a source buffer and a target buffer. In the source buffer, we are assigning the text content "MemoryStream example". Using this variable "sourceBuffer", we are creating an object of MemoryStream. Now, the source buffer will act as a backing store of the MemoryStream object. Then, using MemoryStream, we are calling its Read(..) method which will read the contents from memory stream into the targetBuffer. After this, we are simply getting the contents from tagetBuffer and printing the same on console.

The other concrete "Stream" classes

Apart from these, there are several other Stream classes that are out of the scope of this course. However, here is some brief about those classes.

Other stream classesDescription
NetworkStreamIt is used to read and write over network sockets.
BufferedStreamIt adds buffering capabilities to an existing stream. You may be aware of the "buffering" term like it buffers the incoming contents and provides them at a same rate of consumption. One of the use case of BufferedStream is that it adds an additional capability while reading and writing to a network where significant lags are expected.
IsolatedStorageFileStreamThis is used for reading and writing to a file in an isolated storage. For detailed info visit - Isolated Storage
PipeStreamIt is used to read and write anonymous and named pipes.
CryptoStreamIt is used to link data streams to cryptographic transformations.

Readers and Writers

In previous examples, we did see that we have used some text encoders to support text reading and writing. However, these classes are specially designed for more advanced text encoding and decoding.

The "System.IO" provides a few other classes that are specially designed for reading and writing character-based streams. This is because most of the applications deal with text-based information. These readers and writers take one of the concrete streams. These readers and writers act as a wrapper on the top of various Streams which makes character based reading and writing seamlessly. Let's see a few of these classes.

BinaryReader and BinaryWriter

These are used to read and write primitive data types such as int, float, double as bytes. Let's see BinaryReader and BinaryWriter with an example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("Binary Reader and Writer");
	Console.WriteLine();

    // Let's write some content
    using(var bw = new BinaryWriter(new FileStream("brw.txt", FileMode.Append)))
    {
		bw.Write(24.005);
		bw.Write("Hello World");
		bw.Write('a');
		bw.Write(23.450009876f);
    }

    using(var br = new BinaryReader(new FileStream("brw.txt", FileMode.Open)))
    {
		Console.WriteLine(br.ReadDouble());
		Console.WriteLine(br.ReadString());
		Console.WriteLine(br.ReadChar());
		Console.WriteLine(br.ReadSingle());
    }

    Console.Write("Press any key to continue...");
	Console.ReadKey(true);
  }
}
			

In the above example, you can see that the constructors of both BinaryReader and BinaryWriter classes take a stream instance as a parameter (FileStream in this case). First, we have used BinaryWriter's instance to write a few values to the file. These values are C# primitive data types. It provides a Write(..) method with several overloads with primitive types as arguments.

In the next part, we have opened the same file and start reading the content then we have written it into the file created earlier. Please note that sequence matters here. This means that the sequence in which we have written the primitive data types must be read in the same sequence.

The thing to note here is that the text that is written onto the file is in binary format and not human readable.

StreamReader and StreamWriter

The StreamReader and StreamWriter classes act as a wrapper on the top of Stream and provide us with a seamless character based reading and writing. The difference between BinaryReader and BinaryWriter is that the content written by StreamReader and StreamWriter is human readable. Let's see an example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("Stream Reader and Writer");
		Console.WriteLine();

    using(var sw = new StreamWriter(new FileStream("srw.txt", FileMode.Append)))
    {
      sw.WriteLine("Document");
      sw.Write("Version -");
      sw.WriteLine(4.0);
      sw.WriteLine("-----------------");
      sw.WriteLine("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
      sw.WriteLine("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.");
    }

    using(var sr = new StreamReader(new FileStream("srw.txt", FileMode.Open)))
    {
      Console.WriteLine(sr.ReadToEnd());
    }

    Console.Write("Press any key to continue...");
		Console.ReadKey(true);
  }
}
			

Similar to the previous example, we have defined the FileStream class as the underlying stream for StreamReader and StreamWriter. Using StreamReader's WriteLine(..) method write strings and other data types as well. The StreamReader class is used to read the text-based contents from a file. In this case, we are reading a similar file that we wrote using the ReadToEnd(..) method of the StreamReader class.

StringReader and StringWriter

These classes are specially designed for large strings reading and writing. The StringReader class implements the abstract class TextReader and StringWriter class implements the abstract class TextWriter. Their main difference from other reader and writer classes is that under the hood the information is stored in StringBuilder class. The StringBuilder class is designed for heavy string manipulation with optimization. One use case of these classes is to read and write log files which are text extensive files.

Files and Directories interaction

The "System.IO" provides some of the static and instance classes which are used to interact with files and directories present onto the disk.

The "File" class

The "File" class is a static class and we can perform many operations like file open, create, delete, copy, move, rename, etc. Here are some of the commonly used methods provided by the "File" class.

File class methodsDescription
CopyIt creates a copy of an existing file to a specified location. It can also overwrite a file if that option is specified.
CreateIt creates a new file or overwrites the existing file with newly created file.
DeleteIt deletes an existing file specified.
ExistsIt checks if the specified files exists or not at the specified location.
GetAttributesIt gets the attributes of the specified file like archived, read-only, hidden, encrypted etc.
MoveIt moves the specified file to another location. You can specify a new file name as well.
OpenIt opens a file and returns the underlying FileStream object.

Apart from these frequently used methods, the File class provides methods for instant text reading and writing like ReadAllText(..), ReadLines(..), WriteAllText(..), etc to quickly read and write the text without operating with streams.

Let's see these operations with an example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("File class");
		Console.WriteLine();

    Console.WriteLine("--- Creating a file ---");
    using(var fs = File.Create("file_example_1.txt"))
    {
      var encoder = new UnicodeEncoding();
      byte[] writeBytes = encoder.GetBytes("FileStream write example.");
      fs.Write(writeBytes, 0, writeBytes.Length);
    }

    Console.WriteLine("A new file is created.");

    Console.WriteLine("Opening and reading the created file");
    using(var fs = File.Open("file_example_1.txt", FileMode.Open))
    {
      byte[] readBytes = new byte[128];
      fs.Read(readBytes, 0, readBytes.Length);
      Console.WriteLine(Encoding.ASCII.GetString(readBytes));
    }

    Console.WriteLine("--- Checking if the file exists or not ---");
    Console.WriteLine(File.Exists("file_example_1.txt")? "File exists." : "File does not exists.");

    Console.WriteLine("--- Copying the file with another name ---");
    File.Copy("file_example_1.txt", "file_example_1_copy.txt");

    Console.WriteLine("--- Checking again if the copied file exists or not ---");
    Console.WriteLine(File.Exists("file_example_1_copy.txt")? "File exists." : "File does not exists.");

    Console.WriteLine("--- Deleting the file ---");
    File.Delete("file_example_1.txt");

    Console.WriteLine("--- Checking again if the file exists or not ---");
    Console.WriteLine(File.Exists("file_example_1.txt")? "File exists." : "File does not exists.");

    Console.Write("Press any key to continue...");
		Console.ReadKey(true);
  }
}
			

The "Directory" class

The "Directory" class is also a static class that allows us to do the operations on the directories or folders present in device. Some of the commonly used Directory operations are.

Directory class methodsDescription
CreateDirectoryIt is used to create a directory at the specified location.
DeleteIt is used to delete a directory from a specified location.
ExistsIt checks if the specified directory exists or not at the specified location.
GetCurrentDirectoryIt returns the current working directory of the application.
GetDirectoriesIt returns the names of all the directories present at the specified location.
GetFilesIt returns the names of all the files present at the specified location.
GetLogicalDrivesIt returns the names of all logical drives present in the system.
GetParentIt returns the parent directory of the specified directory.
MoveIt moves a directory to another location.

Let's see some of these Directory class operations with an example.


using System;
using System.IO;
using System.Text;

class Program 
{
  static void Main(string[] args)
  {
    Console.WriteLine("Directory class");
		Console.WriteLine();

    Console.WriteLine("--- Creating a directory ---");
    Directory.CreateDirectory("Directory example");
    Console.WriteLine("--- Directory created ---");

    Console.WriteLine("--- Checking if the directory exists or not ---");
    Console.WriteLine(Directory.Exists("Directory example")? "Directory exists." : "Directory does not exists.");

    Console.WriteLine("--- Deleting the directory ---");
    Directory.Delete("Directory example");
    Console.WriteLine("--- Directory deleted ---");

    Console.WriteLine("--- Checking again if the directory exists or not ---");
    Console.WriteLine(Directory.Exists("Directory example")? "Directory exists." : "Directory does not exists.");

    Console.Write("Press any key to continue...");
		Console.ReadKey(true);
  }
}
			

The "File" and the "Directory" class are static classes. If you want to use the instance classes then there are two other instance classes which provides the same operations as of the "File" and "Directory" classes. These instance classes are "FileInfo" and "DirectoryInfo". You should use these instance classes when you need to repeatedly perform many operation on the same file like logs generation. This is because in case of instance methods the file security check will happen only once.

To Do

* Note : These actions will be locked once done and hence can not be reverted.

1. Track your progress [Earn 200 points]

2. Provide your ratings to this chapter [Earn 100 points]

0