File IO

Article Info

As well as standard in and out, we can also use file streams for reading and writing to files.

This week’s session introduces the following concepts:

  • File Writing (Text)

  • File Reading (Text)

  • Functions (calling)

  • Functions (modifying)

System IO

The System.IO namespace contains methods for performing operations on File objects. This includes operations like creating, modifying and moving files.

Saving to a file

StreamWriter sw = File.CreateText(@"D:\test.txt"); // (1)

<1>: creates a variable of type StreamWriter

In object oriented programming, an instance of an object is used to store state assoicated with that object. You can think of a StreamWriter as the variables type, much like int or float, but rather than representing the concept of a number, this type represents the concept of a file on the file system.

We can use methods on this type to change the contents of the file, much as we used WriteLine to write to standard out:

sw.WriteLine("Hello, World");

When we are done with a file, we need to close the resource, to tell the operating system we are done with it (and flush any changes that haven’t been written to disk).

sw.Close();
Navigate to the filesystem where you saved the file. What is the contents of the file you have created?

Modify the code to write 10 lines to the file.

Files created in this way will overwrite the contents of a file already present on the disk.

Modify the contents of the text file you have created (eg, remove a line or alter the words) in a program like notepad, save the file and close notepad. Re-run your program and examine the file contents.

Using Statements

As noted above, we need to make sure we close objects to ensure that our program handles them correctly. We can use a special construct, using to ensure that the file is closed when no longer required:

using (StreamWriter sw = File.CreateText(@"D:\test.txt"))
{
    // sw is in scope here and can be used
    sw.WriteLine("Hello, world!");

} // sw is automatically closed here

This is conceptually the same as the code we wrote before, the file is opened, we write to it, then the file is closed. The main difference is if something goes like (like an error or exception) the file will be closed when the scope is escaped.

Reading Files

As well as Writing files, we can do the opposite - reading files using a StreamReader:

using (StreamReader sr = new(@"D:\text.txt")) {
    String line = sr.ReadLine();
    Console.WriteLine(line);
}

Implementing Cat

A simple command line program exists in Unix-Like envrioments, the command cat. This command takes several streams of text and joins (concatates) them. We will implement a simpiler version of this utility.

The following code takes input.txt and copies it line-by-line to output.txt. Note that ReadLine() will return null when there are no more lines of text to read.

using (StreamWriter sw = File.CreateText(@"D:\output.txt"))
{
    using (StreamReader rs = new(@"D:\input.txt")) {
        String? line = rs.ReadLine();
        while ( line != null ) {
            sw.WriteLine(line);
            line = rs.ReadLine();
        }
    }
}

Modify this code so that it reads input_1.txt to (and including) input_9.txt and outputs them to output.txt.

Make the files input_1.txt to input_9.txt and write some text in them.

Run your code and make sure that `output.txt` contains what you expect.

Modifying Cat

Although cat is an extremely useful command-line utiltiy for a range of tasks, our version is a little boring for this session. Lets spice it up a little by modifying the text before writing it out again.

This function should be placed

public static string Capitalise(string input)
{
    return input.ToUpper();
}

It is useful to break code into smaller blocks which we can reason about. Much like when we discussed abstraction we can use this to abstract away details into a higher-level concept we can use in our code.

The function I have just given you takes a single argument called input and returns the captialised version of this string. If you give this function the word world it will output WORLD.

We can modify our cat code to make SHOUTY cat — cat which makes all input text upper case.

The code below shows how I have modified my sample code to call Capitalise, modify your version (which works with multiple files) in the same way:

using (StreamWriter sw = File.CreateText(@"D:\output.txt"))
{
    using (StreamReader rs = new(@"D:\input.txt")) {
        String? line = sr.ReadLine();
        while ( line != null ) {
            string line_upper = Capitalise(line);
            sw.WriteLine(line_upper);
        }
    }
}

Strings have many useful methods which we can use, just like I used ToUpper(). Lets make Capitalise more intresting by utilising some of these methods.

  • Strings have a contains method which can be used to check if the string contains a given character or string. Modify capitalise so it only capitalises lines which contain the letter z.

Here is a word list to test with, use this as one of your input files:

input_2.txt
hello
zebra
chicken soup
buzz
An Aztec
Zoo

Iterating Strings

You can iterate over strings using loops. Consider the following code:

    public static int VowelCount(string line)
    {
        int vowelCount = 0;
        foreach (char letter in line)
        {
            if (letter == 'a' || letter == 'e')
            {
                vowelCount++;
            }
        }

        return vowelCount;
    }

Add this function to your code, and write some code in Main to test it.

  • What does the function name imply this code does?

  • What does this function actually do?

How could you modify this code to do what it was intended to do?

Modify the capitalise function to only capitalise lines that have exactly two vowels, test your code.

Extention Tasks

If you are feeling overwhelmed

  • Try adding a file which stores each of the users guesses in a file called history.txt

  • Whenever the user provides a guess, store the guess in the file

  • Add additional infomation (eg, what the real number was) to the history file

If you did not finish the guessing game, you could instead try to get the guessing game working - or you could modify the code I gave you last week.

  • (advanced) How could you ensure that multiple games worth of gusses are stored?

If you are Feeling Confident

Modify your (or my) guessing code to add a high score table:

  1. When the program starts, read the contents of scores.txt - this file should contain the 'best' score encountered so far then a line containing the highest scorer’s name

  2. This should be displayed before the guessing game starts

  3. The user should play the guessing game

  4. If the player beats the high score, they should be prompted for their name - once entered their score and name should be saved to the text file

For this version, don’t worry about handling the case when scores.txt does not already exist - make it in nodepad before your program runs.

(Advanced) If you want a challenge

Implement the high score table described above, but…​

  • Store the infomation on one line rather than two, seperating the score and name with a space. (hint: strings have a split method)

  • Store the best 5 scores, and place the user’s score in the correct place in the file (re-writing the whole file is fine)

  • Handle the case when the file does not exist (hint: File has a way of checking if a file Exists).

(advanced) Game Metrics

If you are the kind of student that rushes through my tasks, I have a special challenge for you. This is actually inspired by my own field of game AI research - it’s often useful to keep track of metrics for game-playing runs for later analysis (eg, how many moves it took to solve a level).

When we considered binary search, we noticed that some numbers could take fewer steps than others. Lets keep track of how many steps it takes the players to guess each number.

  • Keep track of the number being guessed, keep a record of how many guesses it took to guess the number in the following format:

number | guesses_required guesses_required guesses_required...

For example, if the game had generated the number 5, and the player took three attempts to guess the number, the file would look like this:

5 | 3

Persist this across runs of your game. If the number is guessed is picked by the program again then store the resulting value after the existing ones, for example, if the game was run twice more, and the player took 7, then 3 gusses to guess the correct number '5', the file would look like the following:

5 | 3 7 6

This is considerably easier if you have knowledge of advanced data structures (eg, Dictionaries and Lists), however, it’s possible without using these (this is possible with an array of strings).

Given enouph games (and optimal play) what should this file look like?

Graduation Cap Book Open book GitHub Info chevron-right Sticky Note chevron-left Puzzle Piece Square Lightbulb Video Exclamation Triangle Globe