File I/O in C#

File I/O

The Crash Course

  • Reading and writing to files is often called file input and output, or file I/O for short.
  • File I/O in C# is fairly easy to do, compared to other languages like C++ and C.
  • Everything you do will start with the File class.
  • File.WriteAllLines(string filePath, string[] lines); lets you write out a whole bunch of lines to a file all at once.
  • File.WriteAllText(string filePath, string allText); lets you write out the contents of the entire file in a single all at once.
  • When you're doing file I/O, you will likely need to take your data and turn it into a format that works for one of the output methods, and then parse the text coming back in, so that you can reconstruct your objects. In other words, there's more to the typical file writing process than just dumping stuff into a file—you'll also need to get your data into the right format.
  • You can also read and write text-based and binary files in a different way (a little at a time, instead of all at once). For either way, you start with File.OpenRead or File.OpenWrite, wrap the returned FileStream in a TextReader, TextWriter, BinaryReader, or BinaryWriter, depending on what you want. Then go crazy reading or writing. When you're done writing, it is a good idea to call the writer's Flush if you are writing, and call the reader or writer's Close method.

Introduction

One very common task in any programming language is the ability to write information to a file and read it back in. This is often called "file input and output", or simply "file I/O".

Writing to files and reading from them is actually a very simple task in C#. We'll look at three different approaches to file I/O, starting with a really simple and quick way, then moving on to a more sophisticated way to write text-based/human readable files, followed by a way to do binary file I/O.

The classes that we use here are contained in the System.IO namespace, which isn't included in the list at the top, by default. The first step will be to add a using directive at the top for it. Place this at the top of your file with the rest of your using directives:

using System.IO;

The Easy Way

Writing the File

There's one very simple way to read from a file in C#. There's a File class that has already been created for you that you can use to quickly write stuff to a file.

Here's two variations on this method to write data to a file:

string informationToWrite = "Hello persistent file storage world!";
File.WriteAllText("C:/Users/RB/Desktop/test1.txt", informationToWrite); // Change the file path here to where you want it.
 
string[] arrayOfInformation = new string[2];
arrayOfInformation[0] = "This is line 1";
arrayOfInformation[1] = "This is line 2";
File.WriteAllLines("C:/Users/RB/Desktop/test2.txt", arrayOfInformation);

In the first set, we prepare a single string that contains the entire contents of what we want stored in the file, and then use the WriteAllText method in the File class to write the whole thing out. The WriteAllText method requires a file location as well as the text to write out.

In the second set, we prepare an array of strings. We then use the WriteAllLines method, which will output the lines, one at a time, separated by a new line, so the file will end up containing this:

This is line 1
This is line 2

Reading the File

Reading a file back in is just as easy:

string fileContents = File.ReadAllText("C:/Users/RB/Desktop/test1.txt");
 
string[] fileContentsByLine = File.ReadAllLines("C:/Users/RB/Desktop/test2.txt");

This does the same thing, but in reverse. In the first part, the entire contents of the file are "slurped" into the single fileContents variable. In the second part, the whole thing is pulled in as an array of strings, where each line in the file comes in as a different index in the string array.

But Wait, There's More…

This technique is extremely simple and easy to work with, but it's often not quite as simple as this. Often, you'll be working with data besides a single long string, or a set of strings that you want to write to a file.

In these cases, though, it should be easy enough to turn your data into a string or a set of strings. For instance, let's say you have a high score board, where you have multiple scores with the name of the player who got it, along with their score. We can create a file format where we write out each player on a single line, with the player's name and score separated by a comma. (This is a CSV file, by the way.)

So we'd want our file to look like this:

Kelly,1000
Steve,950
Angela,925
Oscar,900
Jim,840

Going back to the tutorial on creating C# classes, let's say we have a HighScore object that we created that has a Name property and a Score property.

To create the strings we want to write out to a file, we can simply do something like this:

HighScore[] highScores = new HighScore[10];
 
// populate and maintain the list of high scores in here as needed...
 
string allHighScoresText = "";
 
foreach(HighScore score in highScores)
{
    allHighScoresText += score.Name + "," + score.Score + "\n";
}
 
File.WriteAllText("highscores.csv", allHighScoresText);

By the way, in case you haven't seen it before, when you are creating strings, there are special characters that you can place in your string. To make a special character, you start off with the '\' character, saying, the next character that I'm going to type is special—not the normal character. In this example, we see '\n', which is a single character (even though we actually write two characters) that is called the "new line" character. This causes it to wrap to the next line. It's something akin to pressing the <ENTER> key on your keyboard.

In reverse, to read the file back in, you can do similar stuff.

You may start off by reading in the entire text of the file, then "parse" (the process of breaking up a block of text into smaller, more meaningful components) the file and turn it back into our list of high scores.

The following code reads in the high scores file we just created and turns it back into a high scores list:

string[] highScoresText = File.ReadAllLines("highscores.csv");
 
HighScore[] highScores = new HighScore[highScoresText.Length];
 
for(int index = 0; index < highScoresText.Length; index++)
{
    string[] tokens = highScoresText[index].Split(',');
 
    string name = tokens[0];
    int score = Convert.ToInt32(tokens[1]);
 
    highScores[index] = new HighScore(name, score);
}

You can see here that the Split method is going to be our good friend when we're reading in stuff from a file. This breaks one string into smaller strings, splitting it where it runs into a particular character (the commas in this case).

Reading Text-Based Files

There's another way to handle text-based file I/O that is quite a bit more complicated, but allows us to do the writing or reading a little at a time, rather than all at once, which is required for the first version. In this section, we'll cover how to do text-based file writing and reading in this way.

Writing the File

We can again use the File class to get us started, but this time, instead of writing out the entire file all at once, we'll just open the file for reading. This gives us a FileStream object, which we'll wrap in a TextWriter object, and use to write out stuff as needed.

FileStream fileStream = File.OpenWrite("C:/Users/RB/Desktop/test3.txt");
TextWriter textWriter = new StreamWriter(fileStream);
 
textWriter.Write(3);
textWriter.Write("Hello");
 
textWriter.Flush();
textWriter.Close();

So here we use the OpenWrite method, and get back a FileStream object. We can work with the FileStream object directly, but it is really low-level. Like writing bytes. We want something more advanced, to make life easier for us, so we wrap our FileStream object in a TextWriter object. (Well, actually, we use a StreamWriter, which is a TextWriter, which is using inheritance.)

Now, with the TextWriter, we can just write text or ints or all sorts of things, using the various overloads for the Write method.

When we're done writing, we want to clean up what we've done. The Flush method makes sure that everything has been written out to the file (the TextWriter might actually not write stuff out to the file until there's enough of it to write a bunch at a time) and then we call Close to release any connections we have to the file.

Reading the File

Reading a file in a similar way is actually somewhat more troublesome than writing it was. The problem is that when we write stuff out to a file, there's no real way to know how it was structured to read stuff back in. So one approach is to fall back to the File.ReadAllLines method we saw before. You can mix and match, as you see fit. But it is still worth looking at how to read in files in a way that mirrors how we wrote out these files.

Just keep in mind that this will be a bit more complicated, because we'll need to work at a bit lower level than we wrote our file out in.

fileStream = File.OpenRead("C:/Users/RB/Desktop/test3.txt");
TextReader textReader = new StreamReader(fileStream);
 
char nextCharacter = (char)textReader.Read();
 
char[] bufferToPutStuffIn = new char[2];
textReader.Read(bufferToPutStuffIn, 0, 2);
string whatWasReadIn = new string(bufferToPutStuffIn);
 
string restOfLine = textReader.ReadLine();
 
textReader.Close();

Like with writing, we start by opening the file, this time with the OpenRead method. We then wrap the FileStream object in a TextReader, and then we're ready to start reading.

To read in a single character, you can use the Read() method. It returns an int, so you'll want to cast it to a char.

You can also read in a whole bunch of text using a different overload of the Read method. You can see from the example above, that to use this method, you need to create an array of chars, and pass that in to the Read method. You also say what index to start writing to in the array (we use 0 here) and how many characters to read in (2, in this case). This code also shows how you can easily convert the character array to a string.

The TextReader also has a ReadLine method that allows you to read in stuff to the end of the line, which automatically returns a string. The code snippet doesn't show it, but there's also a method called ReadToEnd that will pull in the entire rest of the file (starting at wherever you currently are).

Like with writing, when we're done reading, we want to call TextReader.Close() to make sure that we are no longer connected to the file.

Reading and Writing Binary Files

Instead of writing a text-based file, the other choice is to write a binary file. When you write a binary file, you won't be able to open up the file in a text editor. Well, you could, but it won't make any sense. It will just be a pile of bytes, and it will look like gibberish. But binary files have a couple of advantages. First, binary files usually take up less space than text-based files. Second, the data is "encrypted" to some extent. Since it isn't text-based, people can't just open it up and read it, so it sort of protects your data. (Note that this is not real, actual encryption, it just happens to obscure your data in a similar way. A talented programmer could still be able to figure out what's in the file.)

Writing the File

You'll see that this is very similar to the text-based version in the previous section. The code to write to a binary file is the same, with the exception of us using a BinaryWriter instead of a TextWriter:

FileStream fileStream = File.OpenWrite("C:/Users/RB/Desktop/test4.txt");
BinaryWriter binaryWriter = new BinaryWriter(fileStream);
 
binaryWriter.Write(3);
binaryWriter.Write("Hello");
 
binaryWriter.Flush();
binaryWriter.Close();

Like before, we open a FileStream that's connected to the file we want with OpenWrite. This time, though, we wrap it in a BinaryWriter instead of a TextWriter.

Next, we can call as many Write methods as we want to output our file. We can call Flush to force the BinaryWriter to write out everything it has and Close to release our connection to the file.

Reading the File

Reading a binary file is actually quite a bit simpler to work with than the text-based version was.

fileStream = File.OpenRead("C:/Users/RB/Desktop/test4.txt");
BinaryReader binaryReader = new BinaryReader(fileStream);
 
int number = binaryReader.ReadInt32();
string text = binaryReader.ReadString();
 
binaryReader.Close();

Again, this is similar to what we've seen in the past. We open the file and wrap the FileStream object with a BinaryReader. Now, though, we can simply call the BinaryReader's various versions of Read***(). ReadInt32 for ints, ReadString for strings, ReadInt16 for shorts, and so on.

Then, like usual, we call Close on the BinaryReader to close our connection to the file.

More About the File Class

While we're talking about the File class, I want to mention a few other things that it can do. The class has a Delete method that allows you to delete files (though it is important that you know that this doesn't bring up a prompt that says "are you sure you want to do this?", it just does it). There are also methods for copying and moving files (Copy and Move respectively). And a final useful one is the ability to check to see if a particular file exists (Exists method). If you try to read from a file that doesn't exist, you will likely run into errors.

What's Next?

File I/O is a very powerful and important thing to be able to do. Nearly every program out there will want to be able to do this in some capacity. There's, of course, more advanced things you can do with file I/O, like reading and writing XML files, and C# provides tools to make writing XML files much simpler.

But while there's a lot to still learn with file I/O, we've covered the most important parts, and it will be enough to get you going with reading and writing files.

As we continue along in the C# Crash Course, we'll continue to look at some of the other more advanced topics of programming in C#. The next thing we'll look at is how to handle errors using exceptions.