5.4. System.IO

The System.IO namespace provides support for input and output. Support for file and directory manipulation is factored across four classes. A Directory and File pair of classes provide support for static members only. We use these classes when we do not have an actual directory or file. To manipulate an actual file or directory object, we use the DirectoryInfo and FileInfo pair of classes. These provide instance member functions to operate on a particular directory or file object. In addition, a Path utility class provides file and directory path string support.

Read and write support is separated into three general categories: the byte-oriented Stream class hierarchy (1), and specialized classes for handling character (2) or binary (3) input/output. Rather than listing the various classes and itemizing each interface, let's walk through an implementation of the file read and write portions of the text query system introduced in Chapter 3. The two primary routines we'll need are the following:

  1. request_text_file(), which requests the path of either a directory or a file. It checks that the file exists and has a file extension, such as .txt, that is supported by our application.

  2. handle_directory(), which confirms that the directory exists. It collects all files with file extensions supported by our application, and it displays the list of files and associated characteristics (length, last opened, and so on).

A path name entered by the user, such as C:fictionsaraby.txt or C:fictions, may represent a file, as in the first example, or a directory, as in the second, or it may be an invalid path name. How do we determine which of these is the case?

// text_file holds a string entered by the user
file_check = File.Exists( text_file );

if ( file_check == false )
{
   if ( Directory.Exists( text_file ))
        return handle_directory();

   Console.WriteLine( "Invalid file: {0}: ", text_file );
}

The File class provides static operations for querying, creating, copying, deleting, moving, and opening files. Exists(string path) returns true if the file represented by path exists, and false if either the file does not exist or path specifies a directory.

The Directory class provides analogous static operations for the support of directories. The Directory instance of Exists(string path) returns true if the directory given by path exists, and false otherwise.

5.4.1. Handling File Extensions: The Path Class

Once we have a valid file name, we still need to ensure that we support the file type. The type of a file, by convention, is indicated by the file extension. For example, a C# program text file is indicated by the .cs file extension, a C++ file by .cpp, an XML file by .xml, a simple text file by .txt, and so on.

The extension is identified by the embedded period within the file name. The file name is interpreted as the sequence of characters following the last directory or volume separator. For example, in the path

@"c:fictionscurrentword.txt"

the file name is word.txt, and the extension is .txt.

The Path utility class provides a set of static operations on a directory or file path string—picking them apart or combining them. (It does not require that the file or directory path actually exist.) We can use the following three static methods to query, retrieve, or change the extension of a file or directory:

  1. bool HasExtension(string path), which returns true if the file name contains an embedded period. Otherwise it returns false.

  2. string GetExtension(string path), which returns the file extension, such as .txt, including the period, or else returns String.Empty.

  3. string ChangeExtension(string path,string newExt), which strips off the extension associated with the file name if the second parameter is null. Otherwise the new extension replaces the old extension. If the file name lacks an extension, the new extension is added.

Here is a brief code sequence that illustrates these and other members of the Path class. The names of the members are reasonably self-describing:

string thePath = @"C:fictionsPhoenixalice.txt";

Console.WriteLine( "The file is named " +
   Path.GetFileNameWithoutExtension( thePath ));

if ( Path.HasExtension( thePath ))
     Console.WriteLine( "It has the extension: " +
                        Path.GetExtension( thePath ));

Console.WriteLine( "The full path is " +
                   Path.GetFullPath( thePath ));

if ( Path.IsPathRooted( thePath ))
     Console.WriteLine( "The path root is " +
                         Path.GetPathRoot( thePath ));

string tempDir = Path.GetTempPath();
Console.WriteLine( "The temporary directory is " + tempDir );

// combine two path strings ...
string tempCombine =
  Path.Combine( tempDir, Path.GetFileName( thePath ));

Console.WriteLine( "Path of file copy " + tempCombine );

// creates the path of a unique file name within the
// system's temporary directory ...

string tempFile = Path.GetTempFileName();
Console.WriteLine( "Temporary file is " + tempFile );

When this code sequence is packaged into a member function and executed, it generates the following output:

The file is named alice
It has the extension: .txt
The full path is C:fictionsPhoenixalice.txt
The path root is C:
The temporary directory is C:DOCUME~1STANLE~1LOCALS~1Temp
Path of file copy C:DOCUME~1STANLE~1LOCALS~1Tempalice.txt
Temporary file is C:DOCUME~1STANLE~1LOCALS~1Temp	mp337.tmp

5.4.2. Manipulating Directories

Given a string that represents a valid directory, we'd like to (1) find all the files in that directory with a file extension that we support, such as .txt, and (2) find all the subdirectories in that directory and examine each of them in turn. The following code fragment does that:

try
{
    // if unable to open for any reason, throws exception
    DirectoryInfo dir = new DirectoryInfo( text_file );

    // holds all supported file types

    ArrayList candidate_files = new ArrayList();

    // holds array of files returned from GetFiles()
    FileInfo [] curr_files;
    foreach ( string ext in m_supported_files )
    {
          // returns a file list from the current directory
          // that matches the given search criteria,
          // such as "*.txt"
        curr_files = dir.GetFiles( "*" + ext );
        candidate_files.AddRange( curr_files );
    }

    // get all subdirectories within our directory
    DirectoryInfo [] directories = dir.GetDirectories();

    // OK: let's do it again
    foreach ( DirectoryInfo d in directories )
          foreach ( string ext in m_supported_files )
          {
            curr_files = d.GetFiles( "*" + ext );
            candidate_files.AddRange( curr_files );
          }

The DirectoryInfo class member GetFiles() comes in two flavors. The empty-signature instance returns an array of FileInfo objects representing each of the files in a directory. A second instance takes a search criteria string and returns only the files that meet those criteria. In our case, we want all files ending with a particular file extension. For example, the following returns all files ending with the .txt file extension:

FileInfo [] curr_files = dir.GetFiles( "*.txt" );

The asterisk (*) serves as a wild card in the search process. Provided that the file name ends with .txt, all characters prior to that are accepted.

GetDirectories() comes in two flavors: the empty-signature instance that returns all directories, and the search criteria instance that returns only those directories that match.

The following member function creates a directory, creates a file and a subdirectory within it, and then deletes them all. Static methods such as Exists() are invoked through the Directory class; instance methods, such as CreateFile() and CreateSubdirectory(), are invoked through a DirectoryInfo class object:

public static void testDirCreateDelete( string workDir )
{
   DirectoryInfo wd;

   // create the directory if it does not exist
   if ( ! Directory.Exists( workDir ))
           wd = Directory.CreateDirectory( workDir );
   else   wd = new DirectoryInfo( workDir );

   // create a file and a subdirectory
   FileStream    f = wd.CreateFile( workDir + "test.txt" );
   Directoryinfo d = wd.CreateSubdirectory( "subdir" );

   // delete directory and its contents
   d.Delete();

   // delete directory and all subdirectories
   f.Close();
   wd.Delete( true );
}

This routine is invoked through the following call:

testDirCreateDelete( Path.GetTempPath() + "foobar" +
                     Path.DirectorySeparatorChar );

DirectorySeparatorChar is a read-only property of the Path class. It is set to the backslash () under Windows; under Unix, to the slash (/); and under the Macintosh operating system, to the colon (:) .

The DirectoryInfo class provides a collection of properties that encapsulate different directory characteristics, such as whether the directory exists (e.g., a disk or CD-ROM drive that is empty, when asked if it exists, returns false). The following code sequence exercises several DirectoryInfo class properties—their names are pretty much self-documenting:

public static void testDirProperties( string workDir )
{
    DirectoryInfo dir = new DirectoryInfo( workDir );
    if ( dir.Exists )
    {
       Console.WriteLine("Directory full name ", dir.FullName);
       // refreshes object --
       dir.Refresh();

       DateTime createTime = dir.CreationTime;
       DateTime lastAccess = dir.LastAccessTime;
       DateTime lastWrite  = dir.LastWriteTime;
       // ... display these ...

       DirectoryInfo parent = dir.Parent;
       DirectoryInfo root   = dir.Root;

       FileInfo []      has_files = dir.GetFiles();
       DirectoryInfo [] has_dirs  = dir.GetDirectories();
    }
}

5.4.3. Manipulating Files

As in the handling of directories, the operations for creating, copying, moving, deleting, querying, and modifying the characteristics of a file are separated into two classes. The File class provides a collection of static members that can be invoked without having an actual file object. The FileInfo class provides instance members to apply to a file object.

The FileInfo class provides a set of properties. These are similar to those of the DirectoryInfo class. For example, the output

Creation Time        : 11/29/2000 7:02 PM
Last Access Time     : 5/7/2001 12:00 AM
Last Write Time      : 3/13/2001 1:59 PM
File Size in Bytes   : 703

is generated from the following code sequence:

DateTime createTime = fd.CreationTime;
DateTime lastAccess = fd.LastAccessTime;
DateTime lastWrite  = fd.LastWriteTime;
long     fileLength = fd.Length;

where fd represents a FileInfo object.

We can query a File using the Attributes property to discover its file attributes, including Archive, Compressed, Encrypted, Hidden, Normal, and ReadOnly, among others. These are all FileAttributes enumerators. The object returned by the Attributes property can be thought of as a bit vector in which each attribute associated with the file is turned either on or off. For example, here is how we might query a FileInfo object as to its attributes:

public static void displayFileAttributes( FileInfo fd )
{
      // Attributes returns a FileAttributes object
      FileAttributes fs = fd.Attributes;

      // use bitwise operators to see if attribute is set
      if (( fs & FileAttributes.Archive ) != 0 )
          // OK: file is archived ...

      if (( fs & FileAttributes.ReadOnly ) != 0 )
            fd.Attributes -= FileAttributes.ReadOnly;

      // ... and so on ...
}

To modify a file attribute, we can either subtract or add the associated enum value, provided of course that we have the required permissions.

5.4.4. Reading and Writing Files

There are multiple ways of opening an existing text file for reading or writing. The actual reading and writing of a text file is done with the StreamReader and the StreamWriter classes, respectively—for example,

public static void StreamReaderWriter()
{
      StreamReader ifile =
          new StreamReader( @"c:fictionsword.txt" );

      StreamWriter ofile =
          new StreamWriter( @"c:fictionsword_out.txt" );

      string str;
      ArrayList textLines = new ArrayList();
      while (( str = ifile.ReadLine()) != null )
      {
            Console.WriteLine( str ); // echo to Console
            textLines.Add( str );     // add to back ...
      }

      textLines.Sort();

      foreach ( string s in textLines )
                ofile.WriteLine( s );

      ifile.Close();
      ofile.Close();
}

If the file is unable to be opened, for whatever reason, an exception is thrown. In production code, then, we should check that we can open both files before attempting to bind them to a StreamReader and a StreamWriter. For example, here is an alternative implementation strategy using the OpenText() and CreateText() member functions of the FileInfo class:

public static void FileOpen( string inFile, string outFile )
{
    FileInfo ifd = new FileInfo( inFile );
    FileInfo ofd = new FileInfo( outFile );

    if ( ifd.Exists && ofd.Exists &&
       ((ofd.Attributes & FileAttributes.ReadOnly)==0))
    {
         StreamReader ifile = ifd.OpenText();
         StreamWriter ofile = ofd.CreateText();

         // rest is the same as above
    }
}

If we wish to append text to an existing output file rather than overwrite the existing text within the file, we invoke FileInfo's AppendText() method rather than CreateText():

StreamWriter ofile = ofd.AppendText();

We use the Stream class hierarchy to read a file as a sequence of bytes rather than as a text file—for example,

FileInfo ifd = new FileInfo( inFile  );
FileInfo ofd = new FileInfo( outFile );

Stream ifile = ifd.OpenRead();
Stream ofile = ofd.OpenWrite();

The File class methods OpenRead() and OpenWrite() return a derived-class instance of Stream. We pass into the Read() method of the Stream an array in which to deposit the bytes, an index indicating where to begin writing the bytes, and a count of the maximum number of bytes to read. Read() returns the number of actual bytes read. A value of 0 indicates the end of the file. Similarly, to write the Stream using the Write() function, we pass in an array holding the bytes, an index indicating where to begin extracting the bytes, and the number of bytes to write:

const int max_bytes = 124;
byte  []  buffer    = new byte[ max_bytes ];
int       bytesRead;

while (( bytesRead =
         ifile.Read( buffer, 0, max_bytes )) != 0 )
{
      Console.WriteLine( "Bytes read: {0}", bytesRead );
      ofile.Write( buffer, 0, bytesRead );
}

Several enum types defined within the System.IO namespace allow us to specify the read/write and sharing attributes of a file when we open it—for example,

Stream ifile = ifd.Open( FileMode.Open, FileAccess.Read,
                         FileShare.Read );


// default FileShare of None
Stream ofile = ofd.Open(FileMode.Truncate,FileAccess.ReadWrite);

There are six FileMode enumerators:

1.
Append, which opens a file if it exists and searches to the end of it. Otherwise it creates a new file.

2.
Create, which creates a new file. If the file exists, it is overwritten.

3.
CreateNew, which creates a new file. If the file exists, an exception is thrown.

4.
Open, which opens an existing file.

5.
OpenOrCreate, which opens a file if it exists; otherwise it creates a new file.

6.
Truncate, which opens an existing file and truncates it to 0 bytes.

The FileAccess enumeration defines three modes of accessing a file being opened (by default, a file is opened for both reading and writing):

  1. Read, which specifies read access. Data can be read from the file, and the current position can be moved. This is the mode for File.OpenRead().

  2. ReadWrite, which specifies read and write access to the file. Data can be both written to and read from the file, and the current position can be moved.

  3. Write, which specifies write access to the file. Data can be written to the file, and the current position can be moved. This is the mode for File.OpenWrite().

The FileShare enumeration defines alternative values for controlling the kind of access that other Stream objects can have to the same file. For example, if a file is opened and FileShare.Read is specified, other users can open the file for reading but not for writing. Here are FileShare's access modes:

  • None, which indicates no sharing of the current file. Any request to open the file (by this process or another process) will fail until the file is closed.

  • Read, which allows subsequent opening of the file for reading.

  • ReadWrite, which allows subsequent opening of the file for reading or writing.

  • Write, which allows subsequent opening of the file for writing.

The Stream class hierarchy allows us to adjust the current position within the stream using Seek(). We can then read and/or write either a byte or a chunk of bytes stored within an array. To discover if a Stream supports read, write, or seek permission, we access the following Boolean properties:

public void StreamSeek( Stream file, int offset )
{
    Console.WriteLine( "CanRead: {0}",  file.CanRead  );
    Console.WriteLine( "CanWrite: {0}", file.CanWrite );
    Console.WriteLine( "CanSeek: {0}",  file.CanSeek  );

    if ( ! file.CanWrite || ! file.CanSeek )
         return;

    // ...
}

For example, let's open our two streams using the File class Open() method with the mode, access, and share enum values we saw earlier:

public void BytesReadWrite( string inFile, string outFile )
{
   FileInfo ifd = new FileInfo( inFile );
   FileInfo ofd = new FileInfo( outFile );
   
   if ( ifd.Exists && ofd.Exists &&
       (( ofd.Attributes & FileAttributes.ReadOnly )==0))

   {
          Stream ifile = ifd.Open( FileMode.Open,
                                      FileAccess.Read,
                                      FileShare.Read );

          Stream ofile = ofd.Open( FileMode.Truncate,
                                      FileAccess.ReadWrite );

          // ... we read the same as before ...
   }
}

The following code sequence illustrates how we can reposition ourselves within a file for both read and write operations. Both Length and Position are Stream properties. Length holds the size in bytes of the file. Position maintains the current position within the stream:

// it's just been written to by ifile;
// let's flush it, just to be on the safe side
ofile.Flush();

long offset = ofile.Length/4 - 1,
     position = 0L;

for ( ; position < ofile.Length; position += offset )
{
    ofile.Seek( position, SeekOrigin.Begin );
    int theByte = ofile.ReadByte();
    ofile.WriteByte((byte)'X' );

    Console.WriteLine( "Position: {0} -- byte replaced: {1}",
                        ofile.Position, theByte.ToChar() );
}

ifile.Close(); ofile.Close();

Seek() takes two arguments. The first represents the byte offset relative to a reference point represented by the second. TSeekOrigin is an enum with three enumerators: (1) Begin, which sets the reference point to the beginning of the stream; (2) Current, which sets it to the current position; and (3) End, <which sets the reference point to the end of the stream.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.104.183