http://www.developer.com/

Back to article

File I/O with Streams - Part 1 - An Excerpt from Inside C#, Second Edition


March 7, 2003

From:
Inside Visual C#, Second Edition
By Tom Archer
ISBN: 0735616485 Published by Microsoft Press

Introduction

This chapter divides neatly into two main topics. First, we.ll consider the classes provided by the .NET Framework classes, which meet the lower-level data transfer requirements of the streams-based I/O framework. These classes further divide into stream classes and file system classes.that is, classes that actually represent data streams, and classes that represent file system objects such as files and directories. Then we.ll look at how you can enhance any custom class to allow it to fit seamlessly into the standard I/O framework. This enhancement is based on a standard attribute that marks your class as capable of being serialized. The serialization process is used in conjunction with the streams classes to stream your custom class objects from one place to another.in memory, to a remote location, or to persistent storage. As part of our exploration of the streams framework, we.ll consider the different types of stream, types of file system objects, and potential application environments, including Microsoft Windows.based and Web-based environments.

Stream Classes

The .NET Framework classes offer a streams-based I/O framework, with the core classes in the System.IO namespace. All classes that represent streams inherit from the Stream class, and the key classes are listed in Table 1.

Table 1 - String and WriteLine Format Specifiers

Class

Description

Stream

The abstract base class Stream supports reading and writing bytes.

FileStream

In addition to basic Stream behavior, this class supports random access to files through its Seek method and supports both synchronous and asynchronous operation.

MemoryStream

A nonbuffered stream whose encapsulated data is directly accessible in memory. This stream has no backing store and might be useful as a temporary buffer.

BufferedStream

A Stream that adds buffering to another Stream, such as a NetworkStream. (FileStream already has buffering internally, and a MemoryStream doesn't need buffering.) A BufferedStream object can be composed around some types of streams to improve read and write performance.

TextReader

The abstract base class for StreamReader and StringReader objects. While the implementations of the abstract Stream class are designed for byte input and output, the implementations of TextReader are designed for Unicode character output.

StreamReader

Reads characters from a Stream, using Encoding to convert characters to and from bytes.

StringReader

Reads characters from a String. StringReader allows you to treat a String with the same API; thus, your output can be either a Stream in any encoding or a String.

TextWriter

The abstract base class for StreamWriter and StringWriter objects. While the implementations of the abstract Stream class are designed for byte input and output, the implementations of TextWriter are designed for Unicode character input.

StreamWriter

Writes characters to a Stream, using Encoding to convert characters to bytes.

StringWriter

Writes characters to a String. StringWriter allows you to treat a String with the same API; thus, your output can be either a Stream in any encoding or a String.

BinaryReader

Reads binary data from a stream.

BinaryWriter

Writes binary data to a stream.

Two classes derived from Stream but not listed in Table 1 are offered in other namespaces. The NetworkStream class represents a Stream over a network connection and resides in the System.Net.Sockets namespace, and the CryptoStream class links data streams to cryptographic transformations and resides in the System.Security.Cryptography namespace.

The design of the Stream class and its derivatives is intended to provide a generic view of data sources and destinations so that the developer can interchangeably use any of these classes without redesigning the application. In general, Stream objects are capable of one or more of the following:

  • Reading The transfer of data from a stream into a data structure, such as an array of bytes
  • Writing The transfer of data from a data structure into a stream
  • Seeking The querying and modifying of the current position within a stream

Note that a given stream might not support all these features. For example, NetworkStream objects don't support seeking. You can use the CanRead, CanWrite, and CanSeek properties of Stream and its derived classes to determine precisely which operations a given stream does in fact support.

The FileStream Class

Let.s dive into some code. Consider this simple use of FileStream, which creates a file, writes out an array of bytes, and closes the file. The application then opens the file again, tests that the stream supports reading, and reads in the bytes one by one, converting each byte to a character and appending it to a string:
using System.IO;

public class StreamsIOApp
{
  public static void Main(string[] args)
  {
    byte[] buf1 = new Byte[]
      {76,101,116,32,116,104,101,114,101,
       32,98,101,32,108,105,103,104,116};

    Stream s = new FileStream("Foo.txt", FileMode.Create);
    s.Write(buf1, 0, buf1.Length);
    s.Close();

    s = new FileStream("Foo.txt", FileMode.Open);
    int i;
    string str = "";
    if (s.CanRead)
    {
      for (i = 0; (i = s.ReadByte()) != -1; i++)
      {
        str += (char)i;
      }
    }
    s.Close();
    Console.WriteLine(str);
  }
}

Here's the output:

Let there be light

Note that we.re using only the Stream (virtual) methods, so we can safely use a Stream reference to the derived FileStream object. Of course, we also could've written the code to use a FileStream reference:

//Stream s = new FileStream(
FileStream s = new FileStream(

It.s no coincidence that the streams methods tend to use bytes and byte arrays.you can visualize a stream as a byte array that might be attached to some memory buffer or to some disk file or device. Streams use the concept of an internal stream pointer: when you open the stream, the stream pointer is normally positioned at the first byte of the stream. Most streams support seeking.the ability to move the internal stream pointer to an arbitrary position. Therefore, instead of the create-write-close-open-read-close pattern, we can avoid closing and opening when we want to switch between writing and reading by seeking in between.

For example, the following continuation code reopens the same file, reports on its length and the position of the internal stream pointer, and then tests to see whether the stream supports seeking. If the stream does, we seek 13 bytes from one of the three relative starting points: SeekOrigin.Begin, SeekOrigin.Current, or SeekOrigin.End. If we write to the stream at that point, we.ll overwrite some of the data that was originally in the stream. We can then seek again.rather than closing and opening.before reading from the file:

byte[] buf2 = new Byte[]
  {97,112,112,108,101,115,97,117,99,101};
s = new FileStream("Foo.txt", FileMode.Open);
Console.WriteLine("Length: {0}, Position: {1}", s.Length, s.Position);
if (s.CanSeek)
{
  s.Seek(13, SeekOrigin.Begin);
  Console.WriteLine("Position: {0}", s.Position);
  s.Write(buf2, 0, buf2.Length);
}

str = "";
s.Seek(0, SeekOrigin.Begin);
for (i = 0; (i = s.ReadByte()) != -1; i++)
{
  str += (char)i;
}
Console.WriteLine(str);

Here's the output:

Length: 18, Position: 0
Position: 13
Let there be applesauce

Note that if you want to Seek from SeekOrigin.End, you should supply a negative value. However, if you want to Seek from SeekOrigin.Current, you can supply either a positive or negative value depending on which direction you want to go. In addition to using the Length and Position properties, you could even arbitrarily set the length by using Stream.SetLength.

str = "";
s.SetLength(s.Length - 4);
s.Seek(0, SeekOrigin.Begin);
for (i = 0; (i = s.ReadByte()) != -1; i++)
{
  str += (char)i;
}
s.Close();
Console.WriteLine(str);

This is the output:

Let there be apples

A stream must support both writing and seeking for SetLength to work.

Instead of using Seek, we could achieve the same effect by using the Position property:

//s.Seek(0, SeekOrigin.Begin);
s.Position = 0;

Finally, we could append to the end of the file. If you open a stream for appending, the internal stream pointer will be positioned at the end of the stream, not the beginning, as is normally the case. FileMode.Append can be used only in conjunction with FileAccess.Write.any attempt to read from the file will fail and throw an ArgumentException. Hence, once we.ve opened the file for appending and have written out some more data, we must close it and reopen it before reading from it:

byte[] buf4 = new Byte[]
  {32,97,110,100,32,112,101,97,114,115};
s = new FileStream("Foo.txt", FileMode.Append, FileAccess.Write);
s.Write(buf4, 0, buf4.Length);
s.Close();

s = new FileStream("Foo.txt", FileMode.Open);
str = "";
for (i = 0; (i = s.ReadByte()) != -1; i++)
{
  str += (char)i;
}
Console.WriteLine(str);
s.Close();

This is the output:

Let there be apples and pears

If you construct a FileStream object with FileMode.Append and don.t specify the access permissions, the object will be constructed with read permission. Therefore, the following statements are equivalent:

s = new FileStream(
//  "Foo.txt", FileMode.Append, FileAccess.Write);
    "Foo.txt", FileMode.Append);

As a general rule, you should always protect your file-handling operations with the appropriate exception-handling code. In this article, we'll keep this code to a minimum. However, the subject of exception handling is covered in much more detail in Chapter 12, "Error Handling with Exceptions" of my Inside C# book.

The StreamReader and StreamWriter Classes

As you can see, FileStream is OK for reading and writing raw byte (binary) data. If you want to work with character data, classes such as StreamReader and StreamWriter are more suitable. These classes will use a FileStream object in the background, effectively interposing a character-interpolation layer on top of the raw byte processing. Closing the StreamReader/StreamWriter also closes the underlying FileStream:
public class ReadWriteApp
{
  public static void Main(string[] args)
  {
    FileStream s = new FileStream("Bar.txt", FileMode.Create);
    StreamWriter w = new StreamWriter(s);
    w.Write("Hello World");
    w.Close();

    s = new FileStream("Bar.txt", FileMode.Open);
    StreamReader r = new StreamReader(s);
    string t;
    while ((t = r.ReadLine()) != null)
    {
      Console.WriteLine(t);
    }
    w.Close();
  }
}

The output follows:

Hello World

Recall from Table 1 that the StreamReader and StreamWriter classes can use Encoding to convert characters to bytes and vice versa. To write our data to the file with some encoding other than the default, we need to construct the StreamWriter and StreamReader with an Encoding parameter, as shown here:

//StreamWriter w = new StreamWriter(s);
StreamWriter w = new StreamWriter(s, System.Text.Encoding.BigEndianUnicode);

Please note that encoding is discussed in more detail in Chapter 10, "String Handling and Regular Expressions." of my book.

If you want to open a file that's been set to read-only, you need to pass an additional parameter to the FileStream constructor to specify that you only want to read from the file:

s = new FileStream("../../TextFile1.txt", FileMode.Open, FileAccess.Read);

Paths in C#

Because C# treats the backslash the same way as C and C++ do, if you want to specify a path for a file, you have three choices. You can either use double backslashes, as in:
s = new FileStream("C:\\temp\\Goo.txt", FileMode.Create);
...or use forward (Unix-style) slashes:
s = new FileStream("C:/temp/Goo.txt", FileMode.Create);
...or use the "@" character, which is a control-character suppressor:
s = new FileStream(@"C:\temp\Goo.txt", FileMode.Create);

Memory and Buffered Streams

The classes MemoryStream and BufferedStream are both derived from the abstract Stream class, just as FileStream is. Therefore, MemoryStream and BufferedStream share many of the same characteristics and functionality. Both are designed for streaming data into and out of memory rather than persistent storage. Both can be associated with a stream of another kind.such as a file.if required, and thus both can be used to act as a buffer between memory and persistent storage. The MemoryStream class offers methods such as WriteTo, which will write to another stream. Similarly, a BufferedStream object is normally associated with another stream on construction, and when you close the BufferedStream, its contents are flushed to the associated stream. In the following example, we first create a MemoryStream object with an initial capacity of 64 bytes and print some arbitrary property values. Then we write out 64 bytes and report the same properties. We then use MemoryStream.GetBuffer to get a byte array of the entire contents of the stream and print the values, before finally closing the stream:
class MemStreamApp
{
  static void Main(string[] args)
  {
    MemoryStream m = new MemoryStream(64);
    Console.WriteLine("Length: {0}\tPosition: {1}\tCapacity: {2}",
                      m.Length, m.Position, m.Capacity);

    for (int i = 0; i < 64; i++)
    {
      m.WriteByte((byte)i);
    }
    Console.WriteLine("Length: {0}\tPosition: {1}\tCapacity: {2}",
                      m.Length, m.Position, m.Capacity);

    Console.WriteLine("\nContents:");
    byte[] ba = m.GetBuffer();
    foreach (byte b in ba)
    {
      Console.Write("{0,-3}", b);
    }

    m.Close();
  }
}

Here's the output:

Length: 0       Position: 0     Capacity: 64
Length: 64      Position: 64    Capacity: 64

Contents:
0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
60 61 62 63

Given that the stream is already up to capacity, let's see what happens if we write some more data to it. Will it generate a runtime exception? Will it overrun into unallocated memory? Or will it just dynamically resize the buffer? The last scenario is, of course, the correct one.which you.ll realize if you've examined the previous output closely. Although we set an initial capacity of 64, this wasn.t the initial size of the memory buffer because the buffer was initialized to 0. So, clearly, the buffer is dynamically resized automatically.

string s = "Foo";
for (int i = 0; i < 3; i++)
{
  m.WriteByte((byte)s[i]);
}
Console.WriteLine("\nLength: {0}\tPosition: {1}\tCapacity: {2}",
                  m.Length, 
                  m.Position, 
                  m.Capacity);

Here's the output:

Length: 67      Position: 67    Capacity: 256

As you can see, the minimum block size is 256 bytes.

Finally, let.s associate this MemoryStream with a FileStream before closing it:

FileStream fs = new FileStream("Goo.txt", 
                                FileMode.Create, 
                                FileAccess.Write);
m.WriteTo(fs);
m.Close();

Now let.s do the same thing with a BufferedStream object. Although generally similar, there are some subtle differences between MemoryStream and BufferedStream. First, we can construct a BufferedStream only by initializing it to some other existing Stream.in this example, a file. The buffer in a BufferedStream is managed differently than that in the MemoryStream: if we don.t specify an initial size when we construct the object, it defaults to 4096 bytes. Also, if we've written some data to the BufferedStream and want to read it back, we must use the Read method.because there is no GetBuffer method. Using the Read method means making sure the internal stream pointer is positioned correctly.in this case, at the beginning of the stream.before attempting the read:

class BufStreamApp
{
  static void Main(string[] args)
  {
    // Create a FileStream for the BufferedStream.
    FileStream fs = new FileStream("Hoo.txt",
        FileMode.Create, FileAccess.ReadWrite);
    BufferedStream bs = new BufferedStream(fs);
    Console.WriteLine("Length: {0}\tPosition: {1}",
                      bs.Length, bs.Position);

    for (int i = 0; i < 64; i++)
    {
      bs.WriteByte((byte)i);
    }
    Console.WriteLine("Length: {0}\tPosition: {1}",
                      bs.Length, bs.Position);

    // Reset to the beginning and read the data.
    Console.WriteLine("\nContents:");
    byte[] ba = new byte[bs.Length];
    bs.Position = 0;
    bs.Read(ba, 0, (int)bs.Length);
    foreach (byte b in ba)
    {
      Console.Write("{0,-3}", b);
    }

    // Write some more, exceeding capacity.
    string s = "Foo";
    for (int i = 0; i < 3; i++)
    {
      bs.WriteByte((byte)s[i]);
    }
    Console.WriteLine("\nLength: {0}\tPosition: {1}\t",
                      bs.Length, bs.Position);

    for (int i = 0; i < (256-67)+1; i++)
    {
      bs.WriteByte((byte)i);
    }
    Console.WriteLine("\nLength: {0}\tPosition: {1}\t",
                      bs.Length, bs.Position);

    bs.Close();
  }
}

The console output is listed here:

Length: 0       Position: 0
Length: 64      Position: 64

Contents:
0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
60 61 62 63

Length: 67      Position: 67

Note that the Hoo.txt file will have the same contents as those in the MemoryStream example. We didn.t have to explicitly write from the BufferedStream to the FileStream as we did for MemoryStream because the BufferedStream must be associated with another stream in the first place. All writes to the BufferedStream will be buffered for writing to the associated stream, and merely closing the BufferedStream object is enough to flush any pending writes to the file.

String Readers and Writers

Objects of the String class are immutable but that you can process mutable objects of the StringBuilder class. The functionality offered by the StringReader and StringWriter classes overlaps somewhat with that of both the MemoryStream and BufferedStream classes and the String and StringBuilder classes. You can use StringWriter to build a mutable string in memory and extract from it either a String or a StringBuilder class. StringWriter has a Write and a WriteLine method that function almost identically to those in the Console class, and you can build the internal string in a StringWriter by using any technique that works with a raw string. For example:
class StringReadWriteApp
{
  static void Main(string[] args)
  {
    StringWriter w = new StringWriter();
    w.WriteLine("Sing a song of {0} pence", 6);
    string s = "A pocket full of rye";
    w.Write(s);
    w.Write(w.NewLine);
    w.Write(String.Format(4 +" and " +20 +" blackbirds"));
    w.Write(new StringBuilder(" baked in a pie"));
    w.WriteLine();
    w.Close();
    Console.WriteLine(w);
  }
}

Here's the output:

Sing a song of 6 pence
A pocket full of rye
4 and 20 blackbirds baked in a pie

Clearly, when we pass the StringWriter reference to Console.WriteLine, the ToString method is being invoked. Note the call to StringWriter.Close in the preceding code. Although there's a Flush method in the StringWriter class, flushing the stream won't flush its underlying encoder unless you explicitly call Close. Closing the stream will automatically flush it and will ready it for destruction by calling Dispose. It.s also instructive to step through this code in the debugger, where you.ll clearly see that the StringWriter contains an internal StringBuilder field named _sb, which in turn contains a String field named m_StringValue. Figure 1 shows these fields.



Figure 1 : StringWriter, StringBuilder, and String. (Click here for larger image)

Let.s continue with this example to modify the string a little more: get a reference to the internal StringBuilder field, and make changes to the StringWriter via that reference, as shown next. Keep the call to the Close method at the bottom, after you.ve finished working with the stream.

StringBuilder sb = w.GetStringBuilder();
int i = sb.Length;
sb.Append("The birds began to sing");
sb.Insert(i, "When the pie was opened\n");
sb.AppendFormat("\nWasn't that a {0} to set before the King", "dainty dish");
Console.WriteLine(w);

The additional output from this block of code is shown here:

Sing a song of 6 pence
A pocket full of rye
4 and 20 blackbirds baked in a pie
When the pie was opened
The birds began to sing
Wasn't that a dainty dish to set before the King

The StringReader class is very simple. We can construct a StringReader from the string in the StringWriter and then use the Read, ReadLine, ReadBlock, and ReadToEnd methods to read characters from the string. This additional code will produce the same output as the previous code block did:

Console.WriteLine();
StringReader r = new StringReader(w.ToString());
string t = r.ReadLine();
Console.WriteLine(t);
Console.Write((char)r.Read());
char[] ca = new char[37];
r.Read(ca, 0, 19);
Console.Write(ca);
r.ReadBlock(ca, 0, 37);
Console.Write(ca);
Console.WriteLine(r.ReadToEnd());
r.Close();
w.Close();

Read will read either one character or a block of characters.in which case, it behaves exactly as ReadBlock does.

Binary Readers and Writers

Recall that the StreamWriter class provides a text-interpolation layer on top of another stream, such as a FileStream. The StreamWriter.Write method is heavily overloaded. Thus, not only can we pass it text, we also can pass it data of type char, int, float, or any other standard type.as well as an open-ended (params) number of objects and anything derived from those objects. Of course, this works similarly to the way that Console.Write works: all data of all types is converted to text before being written to the stream. The BinaryWriter class allows you to write data to a stream without this text interpolation so that the data is written in binary form. For example, compare the two files that result from the following code.one written by using StreamWriter, the other with BinaryWriter:
class BinReadWriteApp
{
  static void Main(string[] args)
  {
    Stream s = new FileStream("Foo.txt", FileMode.Create);
    StreamWriter w = new StreamWriter(s);
    w.Write("Hello World ");
    w.Write(123);
    w.Write(' ');
    w.Write(45.67);
    w.Close();
    s.Close();

    Stream t = new FileStream("Bar.dat", FileMode.Create);
    BinaryWriter b = new BinaryWriter(t);
    b.Write("Hello World ");
    b.Write(123);
    b.Write(' ');
    b.Write(45.67);
    b.Close();
    t.Close();
  }
}

You can open the two output files in Notepad or from the Microsoft Visual Studio File|Open|File menu. The contents of Foo.txt are shown here:

Hello World 123 45.67

We.d probably accept that the unreadable stuff after "Hello, World " is the binary numeric data. A quick check of the ASCII table reveals that the decimal value 123 is the character "{", so the rest must be the floating-point value. But what.s that unprintable character at the beginning of the file? If we open the file in Visual Studio instead of in Notepad, Visual Studio will use a binary/hex editor when it recognizes nonprintable characters in the file. Take a look at Figure 2.



Figure 2 - A binary file in the binary/hex editor. (click for larger image)

Now it becomes clearer. The string .Hello, World. is easy to see, followed by 0x20, which is the space; then the decimal 123, which is 0x7B (the character .{.); followed by another space; then 8 bytes of floating-point value. As you can see, the value of the very first byte in the stream is 0x0C, or decimal 12. Anyone with any experience using strings in Microsoft Visual Basic or using BSTRs in COM development will recognize this right away. It.s the length of the following string.including the space at the end.

Naturally, if you plan to write binary data, you.ll want to read back binary data by using the BinaryReader class, as illustrated in the following code. When you do so, remember to keep the Close calls at the end.

b.BaseStream.Position = 0;
BinaryReader r = new BinaryReader(t);
int i = 0;
while (true)
{
  i = b.BaseStream.ReadByte();
  if (-1 == i)
  {
    Console.WriteLine();
    break;
  }
  Console.Write("{0,-4}", i);
}

r.Close();
b.Close();
t.Close();

Here's the output from this revised application:

12  72  101 108 108 111 32  87  111 114 108 100 32  
123 0   0   0   32  246 40  92  143 194 213 70  64

File System Classes

In addition to the Stream class and its various derivatives, the .NET Framework classes also offer a set of file system.related classes for encapsulating information and functionality that's suitable for processing files and directories. These classes reside in the System.IO namespace and are listed in Table 2.

Table 2 - File system classes in the .NET Framework Classes

Class

Description

FileSystemInfo

The abstract base class for FileInfo and DirectoryInfo objects, this class contains methods that are common to both file and directory manipulation. Useful when processing a lot of files and directories.

DirectoryInfo

Supports creation, deletion, and manipulation of directories (only instance methods).

FileInfo

Supports creation, deletion, and manipulation of files (only instance methods).

Directory

Supports creation, deletion, and manipulation of directories. (All methods are static.)

File

Supports creation, deletion, and manipulation of files. (All methods are static.)

Path

Performs operations on a String that contains file or directory path information.the file or directory doesn't need to exist.

Directory and DirectoryInfo

The FileInfo and DirectoryInfo classes encapsulate information about files and directories as well as methods, such as Create, Delete, Open, MoveTo, and CopyTo. These classes two also offer behavior similar to that of CFile in the Microsoft Foundation Classes (MFC) and fstream in the Standard C++ library. The following example gets a DirectoryInfo object from the static Directory.GetCurrentDirectory method and then calls the DirectoryInfo.GetFiles instance method to get a collection of FileInfo objects. Finally, we iterate this collection to report on some arbitrary properties of each file:
public class DirInfoApp
{
  public static void Main(string[] args)
  {
    DirectoryInfo dir = 
      new DirectoryInfo(Directory.GetCurrentDirectory());
    Console.WriteLine("Current Dir: {0}", dir.FullName);

    foreach (FileInfo f in dir.GetFiles())
    {
      Console.WriteLine("{0,-14}{1,10}{2,20}",
                        f.Name, f.Length, 
                        f.LastWriteTime);
    }
  }
}

Here's the output:

Current Dir: C:\InsideCsharp\Chap11\DirInfo\DirInfo\bin\Debug
DirInfo.exe         6144 04/01/2002 21:06:30
DirInfo.pdb        13824 04/01/2002 21:06:30

In addition to using GetCurrentDirectory, we can construct DirectoryInfo objects by using a string for the desired path:

dir = new DirectoryInfo(".");
dir = new DirectoryInfo(@"C:\Winnt");

The first example, of course, will produce the same results as calling GetCurrentDirectory will. Another useful method is GetDirectories, which will return as DirectoryInfo objects a collection of subdirectories of the current directory. The common parentage of DirectoryInfo and FileInfo is clear from the following code:

Console.WriteLine("\n{0,-32}{1}", "Name", "LastWriteTime");
foreach (DirectoryInfo d in dir.GetDirectories())
{
  Console.WriteLine(
  "{0,-32}{1}", d.Name, d.LastWriteTime);
}

This is the output:

Name                            LastWriteTime
$NtUninstallQ301625$            24/12/2001 17:37:43
A5W_DATA                        11/02/2001 15:18:25
addins                          25/11/2001 12:16:25
AppPatch                        24/12/2001 15:14:34
Assembly                        24/12/2001 15:23:47
Config                          25/11/2001 12:17:06
Connection Wizard               07/01/2000 11:27:04
CSC                             25/11/2001 17:56:56
Cursors                         24/12/2001 15:15:07
Debug                           07/01/2002 09:00:23
Downloaded Program Files        16/12/2001 11:15:14
Driver Cache                    07/01/2000 11:27:04
Fonts                           25/11/2001 15:17:08
Help                            24/12/2001 16:03:31
IIS Temporary Compressed Files  06/04/2001 10:46:27
...

The DirectoryInfo class offers a reasonable set of methods for creating, deleting, moving, and so on. For instance, we could create a new directory at some arbitrary location. (In the following example, the new directory is created within the current directory.) We could then create a subdirectory within the new directory, set and then get some attributes, and finally delete both the subdirectory and the newly created parent directory:

dir = new DirectoryInfo("Foo");
if (false == dir.Exists)
  dir.Create();

DirectoryInfo dis = dir.CreateSubdirectory("Bar");
dis.Attributes |= FileAttributes.Hidden | FileAttributes.Archive;

Console.WriteLine("{0,-10}{1,-10}{2}", 
                  dis.Name, dis.Parent, dis.Attributes);

dis.Delete(true);
dir.Delete(true);

The output follows:

Bar       Foo       Hidden, Directory, Archive

Recall that the library also offers the Directory class, which exposes only static methods. Thus, in the previous code, we could've used the static method Directory.Delete instead and achieved the same results:

//dir.Delete(true);
Directory.Delete(dir.Name, true);

The set of methods offered by the Directory class more or less parallels the instance methods in the DirectoryInfo class. The same parallelism is true for the File and FileInfo classes. In both cases, a couple of additional methods that aren't offered by the parallel class exist. For instance, the Directory class offers a GetLogicalDrives method:

string[] sa = Directory.GetLogicalDrives();
Console.WriteLine("Logical Drives:");
foreach (string s in sa)
{
  Console.Write("{0,-4}", s);
}

Here's the output:

Logical Drives:
A:\ C:\ D:\ E:\ F:\

File and FileInfo

In addition to offering a Directory class with only static methods and a DirectoryInfo class with only instance methods, the library offers a File class with only static methods and a FileInfo class with only instance methods. To explore these two classes, we.ll rewrite our example from the "Stream Classes" section that used FileStream, StreamReader, and StreamWriter.first using FileInfo, then using File.

In the first example that follows, we use a FileInfo object and the FileInfo.Create method in place of the overloaded FileStream constructor. Similarly, we later open the same file by using FileInfo.Open in place of the FileStream constructor. When you open a file this way, you must supply the open mode (Append, Create, CreateNew, Open, OpenOrCreate, or Truncate), access permissions (Read, ReadWrite, or Write), and sharing permissions you want to grant to other objects using this file (None, Read, ReadWrite, or Write). As you can see, one advantage of using FileInfo instead of FileStream in this situation is that we can reuse the same FileInfo object.which contains property values such as the filename.and simply regenerate a FileStream object opened in the manner we require:

class FileFileInfoApp
{
  static void Main(string[] args)
  {
//    Stream s = new FileStream("Bar.txt", FileMode.Create);
      FileInfo f = new FileInfo("Bar.txt");
      FileStream fs = f.Create();

      StreamWriter w = new StreamWriter(fs);
      w.Write("Hello World");
      w.Close();

//    s = new FileStream("Bar.txt", FileMode.Open);
      fs = f.Open(FileMode.Open, 
                  FileAccess.Read, 
                  FileShare.None);
      StreamReader r = new StreamReader(fs);
      string t;
      while ((t = r.ReadLine()) != null)
      {
        Console.WriteLine(t);
      }
      w.Close();
      fs.Close();
      f.Delete();
  }
}

The equivalent code using the File class static methods is this:

FileInfo f2 = new FileInfo("Bar.txt");
FileStream fs2 = File.Create("Bar.txt");

StreamWriter w2 = new StreamWriter(fs2);
w2.Write("Goodbye Mars");
w2.Close();

fs2 = File.Open("Bar.txt", 
                FileMode.Open, 
                FileAccess.Read, FileShare.None);
StreamReader r2 = new StreamReader(fs2);
while ((t = r2.ReadLine()) != null)
{
  Console.WriteLine(t);
}
w2.Close();
fs2.Close();
f2.Delete();

One advantage that the FileInfo and File classes have over the FileStream class in this situation is the OpenText, CreateText, and AppendText methods. These methods will return StreamReader or StreamWriter objects; therefore, we can use them as a kind of shortcut, bypassing the need for a FileStream object:

FileInfo f3 = new FileInfo("Bar.txt");
StreamWriter w3 = f3.CreateText();
w3.Write("Farewell Pluto");
w3.Close();

StreamReader r3 = f3.OpenText();
while ((t = r3.ReadLine()) != null)
{
  Console.WriteLine(t);
}
w3.Close();

Parsing Paths

The Path class is designed to enable you to easily perform common operations such as determining whether a filename extension is part of a path and combining two strings into one pathname. A path is a string that provides the location of a file or directory: it doesn't necessarily point to a location on disk. Most members of the Path class don't interact with the file system and don't verify the existence of the file specified by a path string. Path class members that modify a path string have no effect on the names of files in the file system. On the other hand, some of the members of the Path class do validate that a specified path string has the correct form, and they throw an exception if the string contains characters that aren't valid in path strings. All members of the Path class are static and can therefore be called without having an instance of a path. A path can contain absolute or relative location information. Absolute paths fully specify a location: the file or directory can be uniquely identified, regardless of the current location. Relative paths specify a partial location: the current location is used as the starting point when locating a file this way.

The following code illustrates how you might use the Path class to report on various parts of the string that represents the current directory and the name of the current executing module:

using System.Diagnostics;

class TestPathApp
{
  static void Main(string[] args)
  {
    Process p = Process.GetCurrentProcess();
    ProcessModule pm = p.MainModule;
    string s = pm.ModuleName;

    Console.WriteLine(Path.GetFullPath(s));
    Console.WriteLine(Path.GetFileName(s));
    Console.WriteLine(Path.GetFileNameWithoutExtension(s));
    Console.WriteLine(Path.GetDirectoryName(
                       Directory.GetCurrentDirectory()));
    Console.WriteLine(Path.GetPathRoot(
                       Directory.GetCurrentDirectory()));
    Console.WriteLine(Path.GetTempPath());
    Console.WriteLine(Path.GetTempFileName());
  }
}

This is the output:

C:\Data\InsideCsharp\Chap11\TestPath\TestPath\bin\Debug\TestPath.exe
TestPath.exe
TestPath
C:\Data\InsideCsharp\Chap11\TestPath\TestPath\bin
C:\
C:\DOCUME~1\andrew\LOCALS~1\Temp\
C:\DOCUME~1\andrew\LOCALS~1\Temp\tmp115.tmp

Summary

The .NET Framework includes a streams-based I/O subframework that's based on the Stream class and its specialized derivatives. Related classes that support file-based streaming include classes to represent files and directories, with an appropriate range of functionality. We.ve seen how an application can use the streams pattern to stream data between memory buffers, or to persistent storage, with a comprehensive set of options for formatting and encoding the stream. The .NET Framework classes supply two primary stream formatting classes to support both binary and XML-formatted data, as well as a set of character encoding classes. In the second part of this two-part series, we'll cover the topics of Non-console use of streams and serialization.

Downloads

Download demo projects - 98 Kb

For more information:
Inside Visual C#, Second Edition
By Tom Archer
ISBN: 0735616485
Published by Microsoft Press

Reprinted with Permission.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date