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 letterz
.
Here is a word list to test with, use this as one of your input files:
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:
-
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 -
This should be displayed before the guessing game starts
-
The user should play the guessing game
-
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?