7 min read

Files

Crash Course

Introduction

Most programs will eventually have a need to store data to a file or retrieve data out of a file.

This is actually really easy to do in C#, though there are several ways to do it, depending on what you want to accomplish. In this tutorial, we’ll cover the most basic approach in depth, and also touch on a slightly more advanced style as well.

The File Class

The simplest thing we could do to save data to a file is done through the System.IO.File class. To use this, you will want to put a using directive at the top of your file.

using System.IO;

note

In C# 10 (November 2021), this particular using directive will be applied automatically, so you will no longer need to do this manually.

The program below takes input from a user and writes it to a file:

Console.WriteLine("Enter some text to save to a file.");
string input = Console.ReadLine();

File.WriteAllText("C:/Users/RB/Desktop/test.txt", input);

The key part of this program is the File.WriteAllText method call. This is a static method, so you access it directly through the class name in the same way that we have interacted with Console, Convert, and Math.

To read data in from a file, we can use the File.ReadAllText method:

string contentsOfFile = File.ReadAllText("C:/Users/RB/Desktop/test.txt");
Console.WriteLine("The file contained " + contentsOfFile);

There is also a version where you can write a pile of strings to a file, placing each item on its own line, and read in a file into a string array, with each line in the file being its own string:

Score score = new Score("RB", 1000, 15); // Name, points, level

string[] textToSave = new string[3];
textToSave[0] = score.Name;
textToSave[1] = score.Points.ToString(); // Everything has a `ToString` method, defined in `object`.
textToSave[2] = score.Level.ToString();

File.WriteAllLines("C:/Users/RB/Desktop/score.txt", textToSave);

In this case, we’re taking an object of some sort and serializing it to a series of characters that can be placed in a file.

To get it back out, we would deserialize the characters and “reconstitute” the object:

string[] lines = File.ReadAllLines("C:/Users/RB/Desktop/score.txt");

string name = lines[0];
int points = Convert.ToInt32(lines[1]);
int level = Convert.ToInt32(lines[2]);

Score score = new Score(name, points, level);

Other File Operations

You may want to also delete files. That can be done easily like this:

File.Delete("C:/Users/RB/Desktop/score.txt");

And the following lets you check to see if a file exists:

if (File.Exists("C:/Users/RB/Desktop/score.txt"))
{
    // ...
}

String Parsing

The Score example from a moment ago shows an important thing we often need to do: break down a complex object into something that can go in a file, and then pull it back out. Converting some (potentially complex) object or collection of objects into something that can be written out as a series of characters or bytes in a file is called serialization . Pulling it out of the file and reconstructing the object or collection of objects is deserialization .

This is a very large topic. There are even reusable libraries of code out there whose sole purpose is to help with this task. Indeed, many game engines have built-in facilities to assist with art asset data and game world persistence.

So we won’t cover everything you could ever want to know about serialization and deserialization, but we will cover a few useful tools at your disposal.

Usually, the easy part is serialization. Deserialization is usually much harder.

As an example, let’s suppose we had multiple scores in an array or list that we wanted to store. We might come up with a simple scheme for storing the name, score, and level such as the one below:

R2-D2,1000,15
C-3PO,800,12
GONK,0,0

Storing the data in this format is not too bad:

Score[] scores = ...; // Get the scores from somewhere, but that is irrelevant here.

string[] output = new string[scores.Length];
for (int index = 0; index < scores.Length; index++)
{
    Score score = scores[index];
    output[index] = score.Name + "," + score.Points.ToString() + "," + score.Level.ToString();
}

File.WriteAllLines("C:/Users/RB/Desktop/scores.txt", output);

Reading all of this back in is slightly trickier, because even if we use ReadAllLines, we still have a string that contains a name, a level, and a score, all packed together. We have to chop it up.

One way to do that is with string’s Split method:

string[] lines = File.ReadAllLines("C:/Users/RB/Desktop/scores.txt");
List<Score> scores = new List<Score>();

foreach (string line in lines)
{
    string[] tokens = line.Split(","); // Elements that come from a chopped up string are often called tokens, hence the name.

    // At this point, tokens[0] contains the name, tokens[1] contains the
    // text form of the points, and tokens[2] contains the text form of the level.

    string name = tokens[0];
    int points = Convert.ToInt32(tokens[1]);
    int level = Convert.ToInt32(tokens[2]);

    scores.Add(new Score(name, points, level));
}

Some Variations on File Paths

I want to point out a couple of things about file paths before we move on. I’ve been using full paths to my files in this tutorial, of the form "C:/Users/RB/Desktop/file.txt".

The first thing I want to mention is that Linux and Mac both prefer forward slashes, as shown above, and Windows handles them just fine (which is why I went with that style here) but Windows tends to prefer backslashes. In Windows, you might see this same path written as C:\Users\RB\Desktop\file.txt. The catch is that a \ character is used to indicate that the next thing immediately following it is not to be taken literally, but has some other meaning. For example, \n indicates a new line character, and \t indicates a tab character. If you write a string for a path with backslashes, by default, it will attempt to interpret the path separator and the next letter as a special symbol, which doesn’t usually work out at all, and never means what you intended. If you want to use backslashes, then you want to use \\ to indicate that, no, this is not a special sequence after all, but just a simple backslash: "C:\\Users\\RB\\Desktop\\file.txt".

Another option is to put an @ before the string literal, which indicates that you don’t expect any special sequences in the string, and the whole thing should be treated literally: @"C:\Users\RB\Desktop\file.txt".

Another thing that should be pointed out is that you can use a relative path instead of an absolute path if you want: string allScores = File.ReadAllText("scores.egg");. In addition to showing you can read and write files with any extension (.egg) not just .txt, this will attempt to read text from a file called "scores.egg" in the working directory. The working directory is typically the location of the executable, but this can vary. And with relative paths, you can still include directory structure: string settings = File.ReadAllText("Settings/UI.config");. That will start in the working directory and look in a subdirectory called Settings for a file called UI.config.

Streams

Using File is the simplest and easiest way to read and write content to a file. It is far from the only option. I don’t want to spent a lot of time on this next topic, other than to bring it to your attention.

One of the problems with the File.ReadAllText/File.WriteAllText-style file access is that the whole thing must be done all at once. You have to assemble the full contents of the file into a string or string array and then write it out. You have to pull in the entire contents of the file into memory all at once and split it out from there.

Streams are a tool for reading and writing files that allow you to read and write a little at a time. Streams can be used for many other things as well (not just file streams, but network streams, memory streams, etc.) but they’re quite low-level. Streams basically let you read and write byte arrays only. That’s not a very convenient way to work with stuff.

Instead, you usually wrap the stream in another object that can translate high-level commands like, “Write this int and then this bool,” into byte arrays automatically:

FileStream stream = File.OpenWrite("data.txt");
BinaryWriter writer = new BinaryWriter(stream);

writer.Write(42);
writer.Write(true);

writer.Close(); // Need to close these out to get the data flushed out to the file.
stream.Close();

Then to read the data back in:

FileStream input = File.OpenRead("data.txt");
BinaryReader reader = new BinaryReader(input);

int number = reader.ReadInt32();
bool truthValue = reader.ReadBoolean();

reader.Close();
input.Close();

We’re only scratching the surface here, but that hopefully illustrates that there are other models to working with files than just plain File.ReadAllText.