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.IOnamespace: : - Namespace: Note theusing System.IOnamespace 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.IOwhen 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
Maincreates aStreamWriterobject assigned to the variablewriter.A
StreamWriterlinks C# to your computer’s file system for writing, not reading.Files are objects, like a Random, and you use the
newsyntax 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
WriteandWriteLineof theConsoleclass, you can also use a format string with the corresponding methods of aStreamWriter, 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
Closemethod 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
readerusing the more compact syntax withvar:var reader = new StreamReader(userFileName);You can use
varin 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
whiletest conditions so far have been in a sense “backward looking”: We have tested a variable that has already been set. The test withEndOfStreamis 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 awhileloop testing forEndOfStream.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 thenullvalue 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.