Chapter 15

Input/Output

Input/output (I/O) is one of the most common operations performed by computer programs. Examples of I/O operations include

  • ? creating and deleting files
  • ? reading from and writing to a file or network socket
  • ? serializing (or saving) objects to persistent storage and retrieving the saved objects

Support for I/O has been available since .NET Framework 1.0 in the form of the System.IO namespace and its subnamespaces. This chapter presents topics based on functionality and selects the most important members of System.IO namespace.

File and directory handling and manipulation are the first topic in this chapter. Here you learn how to create and delete files and directories as well as manipulate their attributes. Next, you learn what a stream is and how to use it in the section “Input/Output Streams.” A stream acts like a water pipe that facilitates data transmission. Reading from and writing to a stream dictate that you do so sequentially, which means to read the second unit of data, you must read the first one first. There are several types of streams. FileStream, NetworkStream, and MemoryStream are some examples of streams. To make lives easier, there are utility classes for manipulating these streams so you don’t have to use them directly. We take a look at these utility classes too.

File and Directory Handling and Manipulation

The .NET Framework class library comes with File and Directory classes for creating and deleting files and directories, respectively. In addition, with File and Directory you can manipulate a file or directory, such as checking if a file/directory exists. In addition, there is also a FileSystemInfo class that is the parent class for FileInfo and DirectoryInfo. FileInfo offers much of the functionality in File and DirectoryInfo provides methods that do what Directory’s methods can do. It is sometimes confusing for a beginner to choose which one to use.

The following subsections elaborate what you can do with File and Directory.

Creating and Deleting A File

To create a file you use the Create method of System.IO.File. Here is its signature.

public static FileStream Create(string path)

For example, the following snippet creates a file and assigns it to a FileStream:

string fileName = @"C:usersjaydenwintemp.txt";
try
{
    FileStream fs = File.Create(fileName);
}
catch (IOException e)
{
    Console.WriteLine(e.Message);
}
finally
{
    // call fs.Close() here
}

If the file to be created already exists, the Create method will overwrite the file with a new file. Also, most of the time, you’ll need to put File.Create in a try block as it will throw an IOException if the operation fails, for example if you don’t have permissions to create files in the specified directory.

A more convenient syntax for working with File is to use the using statement like this:

using (FileStream fs = File.Create(fileName))
{
    // do something with the FileStream here
}

With this syntax you don’t have to worry about closing it as the using statement will take care of it. However, the Create method may throw an exception if it fails to create the file. As such, even when using the using statement, you still need to enclose your code in a try block like this:

try
{
    using (FileStream fs = File.Create(fileName))
    {
        // do something with the FileStream here
    }
}
catch (Exception e)
{
    // handle exception
}

Creating and Deleting A Directory

The System.IO.Directory class offers static methods for creating and deleting a directory and a subdirectory. To create a directory, use the CreateDirectory method.

public static DirectoryInfo CreateDirectory(string path)

CreateDirectory returns a DirectoryInfo that exposes a number of directory attributes, such as the creation time, the last access time, and so on. In addition, the DirectoryInfo class offer methods for retrieving and manipulating the files in the current directory. You will learn more about the DirectoryInfo class in the section “Working with File and Directory Attributes” later in this chapter.

CreateDirectory may throw an exception if the operation didn’t complete successfully. For instance, if there was insufficient permission to carry out the task, the method will throw an UnauthorizedAccessException. Likewise, trying to create a directory with the same path as an existing file will throw an IOException.

To delete a directory, use the Delete method of the Directory class. There are two overloads for this method:

public static void Delete(string path)
public static void Delete(string path, boolean recursive)

With the first overload, the directory must be writable and empty. Trying to delete a directory that is not empty will throw an IOException. Using the second overload, however, you can delete a non-empty directory if you pass true as the second argument. Note that the second overload will fail if the directory contains a read-only file.

Working with File and Directory Attributes

The FileInfo and DirectoryInfo classes are used for working with files and directories, respectively. With FileInfo, you can create and delete a file, even though the code would be less brief than if you’re using the File class. Here is how to create a file with FileInfo. You first need to create an instance of FileInfo.

String path = @"C:	emp
ote.txt";
FileInfo fileInfo = new FileInfo(path);
using (FileStream fileStream = fileInfo.Create())
{
    // do something with fileStream
}

There are benefits of choosing FileInfo over File. For example, you can easily get the file extension by calling the Extension property on a FileInfo. In addition, you can obtain a FileInfo's parent directory by invoking the Parent property. This is not to mention that its Length property returns the file size in bytes, CreationTime returns the creation time, isReadOnly indicates if the file is read-only, and Exists returns a boolean that indicates if the file exists.

The last property, Exists, probably caught your attention. How can a file not exist if you have created a FileInfo that points to the file? The truth is, creating a FileInfo simply creates an object in memory and does not create a file. You still need to call Create to create the file. Of course, if you pass the path to an existing file when creating a FileInfo, its Exists property will return true.

The DirectoryInfo class is similar to FileInfo and offers a similar set of properties, such as CreationTime, Exists, Extension, and Parent. DirectoryInfo also provides methods for creating and deleting a directory, obtaining the list of subdirectories, and retrieving the list of files in the directory. Here is the signature of GetFiles, which returns an array of FileInfos.

public FileInfo[] GetFiles()

And, here is the signature of GetDirectories, which returns an array of DirectoryInfos, each of which represents a subdirectory of the current directory.

public FileInfo[] GetDirectories()

Listing Files in A Directory

The easiest way to get the file list in a directory is to use the GetFiles method of the Directory class. Here is code that prints all files in C drive.

string[] paths = Directory.GetFiles(@"C:");
foreach (string path in paths)
{
    Console.WriteLine(path);
}

Note that Directory.GetFiles returns a string array. Each element of the array contains a full path to the file, e.g. C:markets.doc.

Alternatively, you can create a DirectoryInfo and call its GetFiles method, like this:

DirectoryInfo directoryInfo = new DirectoryInfo("C:\");
FileInfo[] files = directoryInfo.GetFiles();
foreach (FileInfo file in files)
{
    Console.WriteLine(file.Name);
}

DirectoryInfo.GetFiles returns an array of FileInfos, unlike Directory.GetFiles that returns a string array.

Copying and Moving Files

Copying a file is easy. You can create a copy of a file by calling the static method Copy on the File class. For example, the following single line of code creates a copy of market.pdf in C: emp to market2.pdf in C: emp.

File.Copy(@"C:	empmarket.pdf", @"C:	empmarket2.pdf");

To move a file, use the Move static method in File. This code snippet moves C: emp esearch.pdf to C: esearch.pdf.

File.Move(@"C:	emp
esearch.pdf", @"C:
esearch.pdf");

Input/Output Streams

I/O streams can be likened to water pipes. Just like water pipes connect city houses to a water reservoir, a stream connects C# code to a “data reservoir.” This “data reservoir” is called a sink and could be a file, a network socket, or memory. The good thing about streams is you employ a uniform way to transport data from and to different sinks, hence simplifying your code. You just need to construct the correct stream.

All streams derive from the System.IO.Stream absract class. You do not work with Stream directly, but rather use one of its descendants like FileStream, MemoryStream, or NetworkStream. You’ve seen FileStream in action in the previous section. When you create a file using File.Create, for example, a FileStream is created for you that allows you to write to the file.

Working with streams directly is hard as you’ll have to manage the stream of data yourself. Fortunately, the System.IO namespace provides several utility classes to work with a stream. Each utility class falls into one of two groups, it’s either a reader or a writer.

The following are the most commonly used readers and writers.

  • StreamReader. A utility class for reading characters from a stream.
  • StreamWriter. A utility class for writing characters to a stream.
  • BinaryReader. A utility class for reading binary data from a stream.
  • BinaryWriter. A utility class for writing binary data to a stream.

All these reader and writer classes take a Stream as an argument to their constructor, so you just need to make sure you pass the correct stream when creating a reader or a writer. For instance, if you want to read a text file, you need to obtain a FileStream that points to the file. Most often, a FileStream is created for you when you call one of the methods in the File class, such as Create. In addition, methods in the File class may already return a reader or a writer without you having to explicitly create a FileStream. For instance, the File class’s OpenText method returns a StreamReader that is linked to the underlying file.

The following sections show how to read and write characters and binary data from a file.

Reading Text (Characters)

You use the StreamReader class to read characters from a stream. Most of the time, you’ll be working with file streams as these are the most popular type of stream. However, there are other kinds of streams too, such as network streams and memory streams.

You can create a StreamReader by passing a Stream to its constructor:

StreamReader streamReader = new StreamReader(stream);

However, you may not need to create a StreamReader explicitly. The OpenText method of the File class, for instance, returns a StreamReader that is associated with a FileStream. Therefore, you can call OpenText and obtain a StreamReader, like so:

StreamReader reader = File.OpenText(path)

This is the same as creating a FileStream and pass it to the StreamReader class’s constructor:

FileStream fileStream = [create/obtain a FileStream];
StreamReader streamReader = new StreamReader(fileStream);

Regardless of how you create a StreamReader, once you have an instance, you can call the various Read methods on the StreamReader. This Read method overload returns the next character in the stream. Here is its signature.

public override int Read()

Note that it returns the character as an int, so to print the character you need to cast it to a char, like this:

StreamReader streamReader = ...
char c = (char) streamReader.Read();

Reading one character at a time is probably not the most efficient way to go. Often times, you want to read in many characters in one read. For this, you can use this overload of the Read method:

public override int Read(char[] buffer, int index, int count)

This Read method overload reads the next count characters from the stream and copy them to the char array used as the first argument. The index argument (the second argument) indicates the start of char array to write to. If index is zero, then the first element of the array will get the first character read. The method returns the number of characters actually read.

Here is an example of reading a block of characters from a stream.

StreamReader streamReader = ...
char[] buffer = new char[100];
streamReader.Read(buffer, 0, 100);
// buffer now contains characters read from the stream

There is also a ReadLine method that reads a line of text from the stream and return it as a string. Its signature is as follows.

public override string ReadLine()

Listing 14.1 shows code that reads characters from a text file.

Listing 14.1: Reading characters from a file

using System;
using System.IO;

namespace StreamReaderExample
{
    class Program
    {
        static void Main(string[] args)
        {
            String path = "C:\temp\today.txt";
            try
            {
                using (StreamReader reader = File.OpenText(path))
                {
                    string line;
                    // Read and display lines from the file 
                    while ((line = reader.ReadLine()) != null)
                    {
                        Console.WriteLine(line);
                    }
                }
            }
            catch (IOException e)
            {
                Console.Write(e.Message);
            }

            Console.ReadKey();
        }
    }
}

The code in Listing 14.1 opens the today.txt file in C: emp and reads and prints each line of text in the file. To test this code, make sure you create a today.txt file in the forementioned directory.

Writing Text (Characters)

To write text or characters to a stream, use the StreamWriter class. This class offers more than a dozen of Write methods for various data types, so that there is no need to first convert a non-string to a string. There is one for a Single, one for Uint32, one for a char, and so on. Here are some of the signatures of the Write methods:

public virtual void Write(bool value)
public virtual void Write(char value)
public virtual void Write(int value)
public virtual void Write(double value)
public virtual void Write(string value)

There is also an overload that allows you to write a block of characters in one single operation:

public virtual void Write(char[] buffer, int index, int count)

In this case, buffer contains the characters to write, index indicates the start element in the array, and count indicates the number of characters in buffer to write.

There are also WriteLine methods that accept a value. To the end of the value the method adds a line terminator. Here are the signatures of some of the WriteLine method overloads.

public virtual void WriteLine()
public virtual void WriteLine(bool value)
public virtual void WriteLine(char value)
public virtual void WriteLine(int value)
public virtual void WriteLine(double value)
public virtual void WriteLine(string value)

The first overload that does not take arguments is used to add a line terminator to the stream.

The code in Listing 14.2 receives input from the console and writes it to a file. It keeps on reading until the user enters an empty string.

Listing 14.2: Writing text to a file

using System;
using System.IO;

namespace StreamWriterExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(
                "Please type in some text. " + 
                "Keep typing until you're tired." + 
                "Enter an empty line to exit");
            using (StreamWriter writer = 
                new StreamWriter(@"C:	empyoursay.txt"))
            {
                String input = Console.ReadLine();
                while (input.Trim().Length != 0)
                {
                    writer.WriteLine(input);
                    input = Console.ReadLine();
                }
            }
        }
    }
}

Reading and Writing Binary Data

To read binary data from a file you use the BinaryReader class. It’s easy to create a BinaryReader, you just need to pass a stream to its constructor:

BinaryReader reader = new BinaryReader(stream);

Then, you call one of its Read methods. To read an integer, for example, you call its ReadInt16 or ReadInt32 method. To read a double, invoke its ReadDouble method. Other methods of BinaryReader include ReadBoolean, ReadChar, ReadByte, ReadDecimal, ReadInt64, and ReadString.

To write binary data to a file, use BinaryWriter. Like BinaryReader, BinaryWriter is also very easy to create an instance of. You just pass a stream to its constructor:

BinaryWriter writer = new BinaryWriter(stream);

BinaryWriter offers a multitude of Write method overloads. In fact, there is one Write method for each data type, so you can write an integer, a double, a decimal, and so on.

The example in Listing 14.3 shows how you write ten integers to the numbers.dat file in C: emp and read them back.

Listing 14.3: Reading and writing binary data

using System;
using System.IO;

namespace BinaryReaderWriterExample
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:	emp
umbers.dat";
            Random random = new Random();
            FileStream fileStream = File.OpenWrite(path);
            using (BinaryWriter writer =  
                    new BinaryWriter(fileStream))
            {
                for (int i = 0; i < 10; i++)
                {
                    writer.Write(random.Next(0, 100));
                }
            }

            fileStream = File.OpenRead(path);
            using (BinaryReader reader =  
                    new BinaryReader(fileStream))
            {
                for (int i = 0; i < 10; i++)
                {
                    int number = reader.ReadInt32();
                    Console.WriteLine(number);
                }
            }

            Console.ReadKey();
        }
    }
}

If you run the program, you’ll see ten numbers between 0 and 100. Each time you run the program, you’ll see a different set of numbers as each number is randomly generated using a Random object.

Summary

Input/output operations are supported through the members of the System.IO namespace. File and directory handling and manipulation are done through the File, Directory, FileInfo, and DirectoryInfo classes. Reading and writing data are achieved through streams. In this chapter you learned how to use these classes to read and write characters and binary data from and to a file: StreamReader, StreamWriter, BinaryReader, and BinaryWriter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.28.197