9.3. Reading Files

In Xamarin Studio, go to project print_first_file. Note that we have added a copy of sample.txt as a project file, so it is an existing file in the project folder. You can open it and see that it is a copy of the file created in the last section.

It will be true of most all the programs for this chapter, but you might check that we have modified the project Output Path to refer to the project folder, in this case with the path ending examples/print_first_file. This means sample.txt will be in the current directory when the program runs.

Run the example program print_first_file/print_first_file.cs, shown below:

using System;
using System.IO;

namespace IntroCS
{
   class PrintFirstFile  // basics of reading file lines
   {
      public static void Main()
      {
         StreamReader reader = new StreamReader("sample.txt");
         string line = reader.ReadLine();  // first line
         Console.WriteLine(line);
         line = reader.ReadLine();         // second line
         Console.WriteLine(line);
         reader.Close();
      }
   }
}

Now you have read a file and used it in a program.

In the first line of Main the operating system file (sample.txt) is associated again with a C# variable name (reader), this time for reading as a StreamReader object. A StreamReader can only open an existing file, so sample.txt must already exist.

Again we have parallel names to those used with Console, but in this case the ReadLine method returns the next line from the file. Here the string from the file line is assigned to the variable line. Each call the ReadLine reads the next line of the file.

Using the Close method is generally optional with files being read. There is nothing to lose if a program ends without closing a file that was being read. [1]

9.3.1. Reading to End of Stream

In first_file.cs, we explicitly coded reading two lines. You will often want to process each line in a file, without knowing the total number of lines at the time when you were programming. This means that files provide us with our second kind of a sequence: the sequence of lines in the file! To process all of them will require a loop and a new test to make sure that you have not yet come to the end of the file’s stream: You can use the EndOfStream property. It has the wrong sense (true at the end of the file), so we negate it, testing for !reader.EndOfStream to continue reading. The example program print_file_lines.cs reads and prints the contents of a file specified by the user, one line at a time:

using System;
using System.IO;

namespace IntroCS
{
   class PrintFileLines  // demo of using EndOfStream test
   {
      public static void Main()
      {
         string userFileName = UI.PromptLine("Enter name of file to print: ");
         var reader = new StreamReader(userFileName);
         while (!reader.EndOfStream) {
            string line = reader.ReadLine();
            Console.WriteLine(line);
         }
         reader.Close();
      }
   }
}
var

For conciseness (and variety) we declared reader using the more compact syntax with var:

var reader = new StreamReader(userFileName);

You can use var in place of a declared type to shorten your code with a couple of restrictions:

  • Use an initializer, from which the type of the variable can be inferred.
  • Declare a local variable inside a method body or in a loop heading.
  • Declare only a single variable in the statement.

We could have used this syntax long ago, but as the type names become longer, it is more useful!

You can run this program. You need an existing file to read. An obvious file is the source file itself: print_file_lines.cs.

Things to note about reading from files:

  • Reading from a file returns the part read, of course. Never forget the side effect: The location in the file advances past the part just read. The next read does not return the same thing as last time. It returns the next part of the file.
  • Our while test conditions so far have been in a sense “backward looking”: We have tested a variable that has already been set. The test with EndOfStream is forward looking: looking at what has not been processed yet. Other than making sure the file is opened, there is no variable that needs to be set before a while loop testing for EndOfStream.
  • If you use ReadLine at the end of the file, the special value null (no object) is returned. This is not an error, but if you try to apply any string methods to the null value returned, then you get an error!

Though print_file_lines.cs was a nice simple illustration of a loop reading lines, it was very verbose considering the final effect of the program, just to print the whole file. You can read the entire remaining contents of a file as a single (multiline) string, using the StreamReader method ReadToEnd. In place of the reading and printing loop we could have just had:

string wholeFile = reader.ReadToEnd();
Console.Write(wholeFile);

ReadToEnd does not strip off a newline, unlike ReadLine, so we do not want to add an extra newline when writing. We use the Write method instead of WriteLine.

9.3.2. Example: Sum Numbers in File

We have summed the numbers from 1 to n. In that case we generated the next number i automatically using i++. We could also read numbers from a file containing one number per line (plus possible white space):

static int CalcSum(string filename)
{
   int sum = 0;
   var reader = new StreamReader(filename);
   while (!reader.EndOfStream) {
      string sVal = reader.ReadLine().Trim();
      sum += int.Parse(sVal);
   }
   reader.Close();
   return sum;
}

Below and in sum_file/sum_file.cs is a more elaborate, complete example, that also exits gracefully if you give a bad file name. If you give a good file name, it skips lines that contain only whitespace.

using System;
using System.IO;

namespace IntroCS
{
   class SumFile  // sum a file integers, one per line
   {
      static void Main()
      {
         string filename = UI.PromptLine(
                              "Enter the name of a file of integers: ");
         if (File.Exists(filename)) {
            Console.WriteLine("The sum is {0}", CalcSum(filename));
         }
         else {
            Console.WriteLine("Bad file name {0}", filename);
         }
      }

      /// Open, read and close the named file and
      /// return the sum of an int from
      /// each line that is not just white space.
      static int CalcSum(string filename)
      {
         int sum = 0;
         var reader = new StreamReader(filename);
         while (!reader.EndOfStream) {
            string sVal = reader.ReadLine().Trim();
            if (sVal.Length > 0) {
               sum += int.Parse(sVal);
            }
         }
         reader.Close();
         return sum;
      }
   }
}

A useful function used in Main for avoiding filename typo errors is File.Exists in the System.IO namespace

bool File.Exists(string filenamePath)

It is true if the named files exists in the operating system’s file structure.

You should see the files sum_file/numbers.txt and sum_file/numbers2.txt in the Xamarin Studio project. You can test with them. It is important to test all paths through the program: also do put in a bad name and see that the program exits gracefully, as intended.

For files in the current folder, you can just use the plain file name. For other folders see Path Strings.

9.3.2.1. Safe Sum File Exercise

  1. Copy sum_file.cs to a file safe_sum_file.cs in a new project of yours. Be sure to modify the Output path option to just refer to the project folder! Modify the program: Write a new function with the heading below. Use it in Main, in place of the if statement that checks (only once) for a legal file:

    // Prompt the user to enter a file name to open for reading.
    // Repeat until the name of an existing file is given.
    // Open and return the file.
    public static StreamReader PromptFile(string prompt)
    
  2. A user who completely forgot the file name could be stuck in an infinite loop! Elaborate the function and program, so that an empty line entered means “give up”, and null (no object) should be returned. The main program needs to test for this and quit gracefully in that case.

9.3.3. Example Copy to Upper Case

Here is a simple fragment from example file copy_upper/copy_upper.cs. It copies a file line by line to a new file in upper case:

var reader = new StreamReader("text.txt");
var writer = new StreamWriter("upper_text.txt");
while (!reader.EndOfStream) {
   string line = reader.ReadLine();
   writer.WriteLine(line.ToUpper());
}
reader.Close();
writer.Close();

You may test this in the Xamarin Studio example project copy_upper:

  1. Expand the copy_upper project in the Solution pad. The project includes the input file. You may not see it at first. You need to expand the folder for bin and then Debug. You see text.txt.
  2. To see what else is in the project directory, select “Open Containing Folder” or “Open in Finder” on a Mac. You should see project file text.txt but not upper_text.txt. Leave that operating system file folder open.
  3. Go back to Xamarin Studio and run the project. Now look at the operating system folder again. You should see upper_text.txt. You can open it and see that it holds an upper case version of the contents of text.txt.

This is another case where the ReadToEnd function could have eliminated the loop. [2]

string contents = reader.ReadToEnd();
writer.Write(contents.ToUpper());
[1]If, for some reason, you want to reread this same file while the same program is running, you need to close it and reopen it.
[2]Besides the speed and efficiency of this second approach, there is also a technical improvement: There may or may not be a newline at the end of the very last line of the file. The ReadLine method works either way, but does not let you know the difference. In the line-by-line version, there is always a newline after the final line written with WriteLine. The ReadToEnd version will have newlines exactly matching the input.