7.3. StreamReader

In the sample code (first_file.cs) of Writing Files from last section, you specified a file sample.txt, which currently exists in the present project folder. The code is as seen below:

 1 using System;
 2 using System.IO;
 3
 4 namespace IntroCSCS
 5 {
 6     class Ch07File  // basics of file writing
 7     {
 8         public static void Main()
 9         {
10            StreamWriter writer = new StreamWriter("sample.txt");
11            writer.WriteLine("This program is writing");
12            writer.WriteLine("our first file.");
13            writer.Close();
14         }
15     }
16 }

Now, take a look at the following code (print_first_file.cs) and compare to the code above (first_file.cs):

using System;
using System.IO;

namespace IntroCSCS
{
   class PrintFirstFile  // basics of reading file lines
   {
      public static void Main()
      {
         StreamReader reader = new StreamReader("sample.txt");
         string line = reader.ReadLine();  // first line // string? ==> nullable
         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 file (sample.txt) is again associated with a C# variable name (reader), this time for reading as a StreamReader, instead of a StreamWriter, 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. Such behavior is similar to the Console.ReadLine() that you know about but different as StreamReader.ReadLine() reads the next line from the input stream (or null if the end of the input stream is reached) rather than from user command line input.

Finally, 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.

7.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 below reads and prints the contents of a file specified by the user, one line at a time:

using System;
using System.IO;

namespace IntroCSCS
{
   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. Or, if you have implemented/tested the files by placing the code in your Program.cs, you may use the sample.txt file here. In the latter case, you should see:

Enter name of file to print: sample.txt
This program is writing
our first file.

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. Here we can use the Write method instead of WriteLine.