8.2. StreamWriter#

The FileStream class provides a stream of bytes[] of data for file operations, supporting both synchronous and asynchronous read and write operations. To use the FileStream class in C#, first of all, you need to include the System.IO namespace and then you need to create an instance of the FileStream class in order to use its functionalities to, for example, create a new file or to open an existing file.

For handling files, the StreamWriter class is more popular in writing files and it is very helpful in writing text data into a file. It is easy to use and provides a complete set of constructors [1] and methods to work on it. Specifically, the StreamWriter class in C# is used for writing characters to stream in a particular format.

8.2.1. Writing Files#

Observe the following program (first_file.cs):

 using System;
 using System.IO;

 namespace IntroCSCS
 {
     class Ch07File  // basics of file writing
     {
         public static void Main()
         {
             StreamWriter writer = new StreamWriter("sample.txt");
             writer.WriteLine("This program is writing");
             writer.WriteLine("our first file.");
             writer.Close();
         }
     }
 }

Look at the code:

  • The System.IO namespace: : - Namespace: Note the using System.IO namespace being used at the top. It gives the program access to the functionalities in the System.IO namespace. For your current concern, this means a number of classes that contains many methods that you use.

    • You will always need to be using System.IO when working with files. Here is a slightly different use of a dot, ., to indicate a subsidiary namespace. Under System.IO, there are plenty of classes (with methods defined inside) for system IO operations, such as: - File, - StreamWriter, - FileStream, - StreamReader, - Directory, - Path, etc.

  • The StreamWriter Class: : - variable: The first line of the body of Main creates a StreamWriter object assigned to the variable writer.

    • A StreamWriter links C# to your computer’s file system for writing, not reading.

    • Files are objects, like a Random, and you use the new syntax to create a new one.

    • The StreamWriter class parameter (“constructor”, to be covered in later chapters) gives the name of the file to connect to the program, sample.txt, which is the same as the file name we saw created by the program.

    Warning

    If the file already existed, the old contents are destroyed silently by creating a StreamWriter.

  • writer.WriteLine(): The writer variable is of data type (or just type) StreamWriter and StreamWriter has method WriteLine() just like the Console class. The difference is that Console.WriteLine writes to the console/terminal, whereas StreamWriter.WriteLine writes out a data stream to a file followed by a newline character.

    Note

    Just as you can use a Composite formatting with functions Write and WriteLine of the Console class, you can also use a format string with the corresponding methods of a StreamWriter, and embed fields by using braces in the format string.

  • writer.Close(): The Close() method closes the current StreamWriter object and the underlying stream. The Close() method is important for cleaning up. Until this line, this C# program controls the file, and nothing may be actually written to the operating system file yet: Since initiating a file operation is thousands of times slower than memory operations, C# buffers data, saving small amounts and writing a larger chunk all at once.

    Warning

    The call to the Close method is essential for C# to make sure everything is really written, and to relinquish control of the file for use by other programs. It is a common bug to write a program where you have the code to add all the data you want to a file, but the program does not end up creating a file. Usually this means you forgot to close the file!

8.2.1.1. Directory path#

If you do not use any operating system directory separators in the name ('\' or '/', depending on your operating system), then the file will lie in the current directory. For example, you may create a data directory under your introcscs directory and place all data files in it.

Footnotes

8.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:

 using System;
 using System.IO;

 namespace IntroCSCS
 {
     class Ch07File  // basics of file writing
     {
         public static void Main()
         {
            StreamWriter writer = new StreamWriter("sample.txt");
            writer.WriteLine("This program is writing");
            writer.WriteLine("our first file.");
            writer.Close();
         }
     }
 }

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.

8.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.

8.4. Path Strings#

When a program is running, there is alway a current directory (or, present working directory, pwd). Files in the present working directory can be referred to by their simple names, e.g., sample.txt, so project files can be referred to by their simple names because they are in the same directory.

Referring to files not in the current directory is more complicated. You should be aware from using the Windows Explorer or the macOS Finder that files and directories are located in a hierarchy of directories in the file system. On a Mac, the file system is unified in one hierarchy. On Windows, each storage drive has its own hierarchy.

Files are generally referred to by a chain of directories before the final name of the file desired. A path string (or simply path) is used to represent such a sequence of names. Elements of the directory chain are separated by operating system specific punctuation symbols: In Windows the separator is backslash, \, and on a Mac it is (forward) slash, /. For example, on a Mac the path

/Users/anh

starts with a /, meaning the root or top directory in the hierarchy, and Users is a subdirectory, and anh is a subdirectory of Users (in this case the home directory for the user with login anh). It is similar with Windows, except there may be a drive in the beginning, and the separator is a \, so

C:\Windows\System32

is on C: drive; Windows is a subdirectory of the root directory \, and System32 is a subdirectory of Windows. Each drive in Windows has a separate file hierarchy underneath it.

Paths starting from the root of a file system, with \ or / are called absolute paths or full paths because they begin with the root of the file hierarchy. Since there is always a current directory, it makes sense to allow a path to be relative to the current directory. In that case do not start with the slash that would indicate the root directory. For example, if the current directory is your user home directory, you likely have a subdirectory Downloads, and the Downloads directory might contain examples.zip. From the home directory, this file could be referred to as Downloads\examples.zip in Windows or Downloads/examples.zip on a Mac.

Referring to files in the current directory just by their plain file name is actually an example of using relative paths.

With relative paths, you sometimes want to move up the directory hierarchy: .. (two periods) refers to the directory one level up the chain.

For example, suppose you have a data directory under your introcscs directory, and you place your sample.txt in the data directory. When running your project from the project directory (Ch07File), you would refer to the sample.txt file to read as ..\data\sample.txt in Windows or ../data/sample.txt in macOS. Follow this one step at a time: Starting from the Ch07File project folder, where the program is running, go up one folder (..) to the introcscs folder, then down into the data folder, and refer to the sample.txt file in that folder.

Occasionally you need to refer explicitly to the current directory: It is referred to as .. (a single period).

8.4.1. Paths in C##

The differing versions of paths for Windows and a Mac are a pain to deal with. Luckily C# abstracts away the differences. It has a Path class in the System.IO namespace that provides many handy functions for dealing with paths in an operating system independent way:

For one thing, C# knows the path separator character for your operating system, Path.DirectorySeparatorChar.

More useful is the function Path.Combine, which takes any number of string parameters for sequential parts of a path, and creates a single string appropriate for the current operating system. For example, Path.Combine("bin", "Debug") will return "bin\Debug" or "bin/debug" as appropriate. Path.Combine("..", "data", "sample.txt") will return a string with characters ..\data\sample.txt or ../data/sample.txt.

Even if you know you are going to be on Windows, file paths are a problem because \ is the string escape character. To enter the Windows path solve explicitly you would need to have "..\\data\\sample.txt", or the raw string prefix, @ can come to the rescue: @"..\data\sample.txt".

Path strings are used by the The Directory Class and by the Simple File Handling: File Class. You can look at the Path class in the C# Language Reference for many other operations with path strings.

8.4.2. The Directory Class#

The Directory class is in the System.IO namespace. Directories in the file system are referenced by Path Strings. You can look at the C# language reference for a wide variety of functions in the Directory class including ones to list all the files in a directory or to check if a path string represents an actual directory.