When you think of the file system, you need to consider its two parts: files and directories. The .NET Framework class library tries to treat files and directories in a very similar way. But, obviously, there are things that you can do with one that you can't do with the other. Because of this, the .NET Framework class library has split the functionality of files and directories into two. Well, that is not actually correct, the functionality was split into four: two classes for files and two for directories.
The reason files and directories were split into two classes each is because of the two different ways programmers tend to work with them, either one time quick-and-dirty or over the lifetime of a method, a class, or even an application. Quick-and-dirty operations on a file or directory really don't need the overhead of creating an instance of a class to handle the operation. Instead, the use of static methods seems more appropriate. On the other hand, if the file or directory is going to be around for a while, it makes sense to create a class instance to hold the file or directory. This makes even more sense when you add the wrinkle that when you use static methods, system security has to be checked every time, but with instantiated classes, security only needs to be checked once on the creation of the class object.
The two classes that make up file access are File and FileInfo. The File class contains static methods to access files, whereas you need to create an instance of a FileInfo class to access files. They have much of the same functionality, so selecting one over the other based on functionality does not normally make sense. Instead, you should choose one class over the other based on the number of times the file will be accessed. If it will be accessed one time only, then File makes sense. If you need repeated access to the file, you should probably use the FileInfo class.
As someone who has coded before, you know that you can open, read, and write to files. The .NET Framework class library takes files and the file system in general a step further. It treats files and directories like the objects they are. It provides not only the standard I/O features you have come to expect in a framework, but also ways of dealing with files and directories as a whole. For example, it is possible to copy, move, get information about, and delete complete file and directory objects. With these functions, you now have a way of providing for the maintenance of the file system as a whole and not just the files that make up the system.
You will look at files and directories separately, but you could almost cover them as one, because they have numerous methods and properties in common. In fact, both DirectoryInfo and FileInfo are derived from the same abstract class, FileSystemInfo.
The FileSystemInfo class provides the numerous properties and methods that the DirectoryInfo and FileInfo classes have in common (see Table 8-1).
PROPERTY/METHOD | DESCRIPTION |
---|---|
Attributes | Gets or sets attributes associated with the current file system object. |
CreationTime | Gets or sets creation date and time of current file system object. |
Exists | Determines if the file system object exists. |
Extension | Gets the string extension associated with the current file system object. |
FullName | Gets full name of the current file system object. This will include the file or directories path. |
LastAccessTime | Gets or sets last access date and time of current file system object. |
LastWriteTime | Gets or sets last date and time current file system object was updated. |
Name | Gets the name of the file or the last directory of current file system object. |
Delete() | Deletes the current file system object. |
As you can see, other than the Delete() method, each of the FileSystemInfo class members in Table 8-1 provides information about the file or directory of the current instance. Some even provide you with update abilities.
The Directory and DirectoryInfo classes provide you with a means of maintaining the directory structure under which your program has control. If you've ever worked directly with the directory structure without the aid of some form of framework, then you'll quickly come to appreciate the ease with which you can maintain the directory system using the .NET Framework class library. To prove that it's simple to work with directories in the .NET Framework class library, let's examine a few of the more common methods and properties.
Whether you are using the static methods provided by Directory or the properties and member method of DirectoryInfo will determine if you need to call a constructor. Obviously, calling static member methods does not require you to instantiate a class, and thus there is no need for a constructor.
The constructor for the DirectoryInfo class simply takes the full path to the directory you wish to manipulate as a parameter, though the directory doesn't need to exist if you're creating it. As you continue, you'll see that the Directory static member calls have this same full path as the member's first parameter.
DirectoryInfo *dir = new DirectoryInfo(S"C:\\WinNT\\Temp");
To examine the details of a directory using the DirectoryInfo class, you need to implement the inherited properties of the FileSystemInfo class. On the other hand, if you are implementing the Directory class, the static member methods are a bit different.
// DirectoryInfo implementation: String *Name = dir->FullName; DateTime Created = dir->CreationTime; DateTime Accessed = dir->LastAccessTime; DateTime Updated = dir->LastWriteTime; FileAttributes Attributes = dir->Attributes; // Directory implementation // No equivalent for dir->FullName DateTime Created = Directory::GetCreationTime(S"C:\\WinNT\\Temp"); DateTime Accessed = Directory::GetLastAccessTime(S"C:\\WinNT\\Temp"); DateTime Updated = Directory::GetLastWriteTime(S"C:\\WinNT\\Temp"); // No equivalent for dir->Attributes
Commonly, you are going to want to list all the files and directories that are contained within the current directory. Both Directory and DirectoryInfo provide methods to get all the files and subdirectories separately in two method calls or together in one method call. Notice, though, that the DirectoryInfo implementation returns an Object, whereas the Directory implementation returns complete directory strings.
// DirectoryInfo implementation: DirectoryInfo *subDirs[] = dir->GetDirectories(); FileInfo *files[] = dir->GetFiles(); FileSystemInfo *dirsFiles[] = dir->GetFileSystemInfos(); // Directory implementation String *subDirs[] = Directory::GetDirectories(S"C:\\WinNT\\Temp"); String *files[] = Directory::GetFiles(S"C:\\WinNT\\Temp"); String *dirsFiles[] = Directory::GetFileSystemEntries(S"C:\\WinNT\\Temp");
Three useful methods that Directory has that DirectoryInfo doesn't are as follows:
String *currentDirectory = Directory::GetCurrentDirectory(); Directory::SetCurrentDirectory(currentDirectory); String *logicalDrives[] = Directory::GetLogicalDrives();
These methods get and set the current working directory and get all current logical drives on the system.
A handy auxiliary class that you can use to manipulate the complete directory strings is the Path class. This class contains several static methods to combine, extract, and manipulate path strings. Table 8-2 shows some of the more useful static methods.
METHOD | DESCRIPTION |
---|---|
ChangeExtension() | Changes the extension of the path string. |
GetDirectoryName() | Extracts the directory name out of the path string. Notice that for a directory, this method extracts the parent path. |
GetExtension() | Gets the extension from the filename contained in the path string. |
GetFileName() | Gets the filename or the directory name. |
GetFileNameWithoutExtension() | Gets the extension from the filename contained in the path string. |
GetFullPath() | Gets the absolute path of the path string. |
To extract the filename out of a complete directory string, you would use the following GetFileName() method of the Path class:
String *files[] = Directory::GetFileSystemEntries(path); for (Int32 i = 0; i < files->Count; i++) { Console::WriteLine(Path::GetFileName(files[i])); }
The activities that you will probably do most with directories are checking if the directory exists, creating a directory, moving or renaming an existing directory, and deleting a directory.
// DirectoryInfo implementation: if (dir->Exists) {} dir->Create(); // Notice it creates the directory specified by constructor dir->CreateSubdirectory(S"SubDir"); dir->MoveTo(S"C:\\WinNT\\TempXXX"); // move or rename the current directory tree dir->Delete(); // will fail if directory is not empty dir->Delete(true); // deletes the entire directory tree (security permitting) // Directory implementation if (Directory::Exists(S"C:\\WinNT\\Temp")) {} Directory::CreateDirectory(S"C:\\WinNT\\TempXXX"); Directory::Move(S"C:\\WinNT\\Temp", S"C:\\WinNT\\TempXXX"); Directory::Delete(S"C:\\WinNT\\TempXXX"); Directory::Delete(S"C:\\WinNT\\TempXXX", true);
Listing 8-1 shows the DirectoryInfo class in action and demonstrates many of the functionalities described previously.
Listing 8-1: Working with DirectoryInfo
#using <mscorlib.dll> using namespace System; using namespace System::IO; using namespace System::Text; Int32 main( Int32 argc, SByte __nogc *argv[] ) { if (argc <= 1) { Console::WriteLine(S"Usage: DirInfo <Directory>"); return -1; } StringBuilder *tmppath = new StringBuilder(); for (Int32 i = 1; i < argc; i++) { tmppath->Append(argv[i]); tmppath->Append(S" "); } String *path = tmppath->ToString()->Trim(); DirectoryInfo *dir = new DirectoryInfo(path); if (!dir->Exists) { Console::WriteLine(S"Directory Not Found"); return -1; } Console::WriteLine(S"Name: {0}", dir->FullName); Console::WriteLine(S"Created: {0} {1}", dir->CreationTime.ToShortDateString(), dir->CreationTime.ToLongTimeString()); Console::WriteLine(S"Accessed: {0} {1}", dir->LastAccessTime.ToShortDateString(), dir->LastAccessTime.ToLongTimeString()); Console::WriteLine(S"Updated: {0} {1}", dir->LastWriteTime.ToShortDateString(), dir->LastWriteTime.ToLongTimeString()); Console::WriteLine(S"Attributes: {0}", __box(dir->Attributes)->ToString()); Console::WriteLine(S"Sub-Directories:"); DirectoryInfo *subDirs[] = dir->GetDirectories(); if (subDirs->Count == 0) Console::WriteLine(S"\tNone."); else { for (Int32 i = 0; i < subDirs->Count; i++) { Console::WriteLine(S"\t{0}", subDirs[i]->Name); } } Console::WriteLine(S"Files:"); FileInfo *files[] = dir->GetFiles(); if (files->Count == 0) Console::WriteLine(S"\tNone."); else { for (Int32 i = 0; i < files->Count; i++) { Console::WriteLine(S"\t{0}", files[i]->Name); } } return 0; }
Figure 8-1 shows the results of the DirInfo.exe program.
Figure 8-1: Results of DirInfo.exe
Once you understand how to manage directories, it's not a big leap to manage files. Most of the properties and methods you use to manage files are identical to those you use to manage directories. The big difference, obviously, is that the class names have changed to File and FileInfo. In addition, there are a few additional file-specific methods added and a couple of directory-specific methods removed. There are also several methods to open up files in different ways. You will cover those a little later in the chapter.
Just like directories, having a constructor depends on whether you are using the static methods of File or the instance member methods of FileInfo.
FileInfo *fileinfo = new FileInfo(S"C:\\WinNT\\Temp\\file.dat");
Note | You could also have coded the previous line as FileInfo *fileinfo = new FileInfo(S"file.dat"); so long as the current directory is C:\WinNT\Temp. You can get and set the current directory with the Directory class's GetCurrentDirectory() and SetCurrentDirectory() methods. |
Examining the details of a file while implementing the FileInfo class requires the use of the inherited properties of the FileSystemInfo class. You will see very little difference between the file methods and the directory methods. The File class's static methods are also the same as the directory equivalent, but this time there is a static method to retrieve attributes (see Table 8-3). There is an additional property to get the length of the file out of a FileInfo class but, oddly enough, there is no static method in the File class.
ATTRIBUTE | DESCRIPTION |
---|---|
Archive | This attribute marks a file for archive or backup. |
Directory | The file is a directory. |
Encrypted | For a file, it means it is encrypted. For a directory, it means that all newly created files in the directory will be encrypted. |
Hidden | The file is hidden from normal directory display. |
Normal | The file is normal and has no other attributes set. (Note: This attribute is only valid if it is the only attribute set.) |
Readonly | The file is read-only. |
System | The file is part of the operating system. |
// FileInfo implementation: String *Name = fileinfo->FullName; DateTime Created = fileinfo->CreationTime; DateTime Accessed = fileinfo->LastAccessTime; DateTime Updated = fileinfo->LastWriteTime; FileAttributes Attributes = fileinfo->Attributes; Int64 Length = fileinfo->Length; // physical, uncompressed, and // unclustered size // File implementation // No equivalent for file->FullName DateTime Created = File::GetCreationTime(S"C:\\WinNT\\Temp\\file.dat"); DateTime Accessed = File::GetLastAccessTime(S"file.dat"); DateTime Updated = File::GetLastWriteTime(S"file.dat"); FileAttributes Attributes = File::GetAttributes(S"file.dat"); // No equivalent for file->Length;
Other than open files, which I cover next, the most likely activities you will do with files are check if a file exists, copy or move an existing file, or simply delete a file. You will find that the methods closely resemble those of the directory.
// FileInfo implementation: if (fileinfo->Exists) {} fileinfo->CopyTo(S"C:\\WinNT\\Temp\\file.dat"); fileinfo->CopyTo(S"file.dat", true); // overwrite existing fileinfo->MoveTo(S"C:\\WinNT\\Temp\\file.dat"); // target file can't exist fileinfo->Delete(); // delete the file // File implementation if (File::Exists(S"C:\\WinNT\\Temp\\file.dat")) {} File::Copy(S"C:\\WinNT\\Temp\\file1.dat", S"C:\\WinNT\\Temp\\file2.dat"); File::Copy(S"file1.dat", S"file2.dat", true); //overwrite existing File::Move(S"C:\\WinNT\\Temp\\file1.dat", S"file2.dat"); File::Delete(S"file1.dat");
Caution | Even though the documentation sort of suggests otherwise, the destination of the Move() and MoveTo() methods cannot be a directory. The destination must be a nonexistent filename or a complete path including the filename. |
Listing 8-2 shows the FileInfo class in action and demonstrates many of the functionalities described previously.
Listing 8-2: Working with FileInfo
#using <mscorlib.dll> using namespace System; using namespace System::IO; using namespace System::Text; Int32 main( Int32 argc, SByte __nogc *argv[] ) { if (argc <= 1) { Console::WriteLine(S"Usage: FileInfo <File>"); return -1; } StringBuilder *tmpfile = new StringBuilder(); for (Int32 i = 1; i < argc; i++) { tmpfile->Append(argv[i]); tmpfile->Append(S" "); } String *strfile = tmpfile->ToString()->Trim(); FileInfo *file = new FileInfo(strfile); if (!file->Exists) { Console::WriteLine(S"File Not Found"); return -1; } Console::WriteLine(S"Name: {0}", file->FullName); Console::WriteLine(S"Created: {0} {1}", file->CreationTime.ToShortDateString(), file->CreationTime.ToLongTimeString()); Console::WriteLine(S"Accessed: {0} {1}", file->LastAccessTime.ToShortDateString(), file->LastAccessTime.ToLongTimeString()); Console::WriteLine(S"Updated: {0} {1}", file->LastWriteTime.ToShortDateString(), file->LastWriteTime.ToLongTimeString()); Console::WriteLine(S"Length: {0}", __box(file->Length)->ToString()); Console::WriteLine(S"Attributes: {0}", __box(file->Attributes)->ToString()); return 0; }
Figure 8-2 shows the results of the FileInfo.exe program.
Figure 8-2: Results of FileInfo.exe
There is no shortage of ways that you can open a file using the .NET Framework class library. There are 14 methods combined in the File and FileInfo class (see Table 8-4). Many of these methods have numerous parameter combinations. Both File and FileInfo use the same 7 method names and each of the methods with the same name do the same thing. Though the methods have the same name, the parameters passed differ, or at least the first parameter differs.
METHOD | DESCRIPTION |
---|---|
Open() | Creates a FileStream to a file providing a plethora of read/write and share privilege options |
Create() | Creates a FileStream providing full read and write privileges to a file |
OpenRead() | Creates a read-only FileStream to an existing file |
OpenWrite() | Creates a write-only unshared FileStream to a file |
AppendText() | Creates a StreamWriter that appends text to the end of an existing file |
CreateText() | Creates a StreamWriter that writes a new text file |
OpenText() | Creates a StreamReader that reads from an existing file |
There always seems to be one exception. The File::Create() has an overloaded method that has a buffer size parameter that the FileInfo class's Create() method lacks.
You will cover FileStream, StreamWriter, and StreamReader later in this chapter.
Of these 14 (7 2) methods, only 2 actually take any parameters (other than the name of the file you wish to open for the static methods). Basically, the .NET Framework class library provides 2 equivalent file open methods and 12 shortcuts.
There are only two root open methods in the .NET Framework class library: File::Open() and FileInfo::Open(). These methods are virtually the same, except the File::Open() method has one additional parameter: the path to the file you want to open. The FileInfo::Open() method gets this information from its constructor.
The Open() method is made up of three overloaded methods. Each overload provides progressively more information about how you want the file opened. The first overload takes as a parameter the file mode with which you wish to open the file (see Table 8-5). Because the other two parameters are not specified, the file will open by default with read/write access and as unshared.
FILEMODE | DESCRIPTION |
---|---|
Append | Opens a file if it exists and sets the next write point to the end of the file. If the file does not exist, it creates a new one. You can only use FileMode::Append with a file access of write-only, as any attempt to read throws an ArgumentException. |
Create | Creates a new file. If the file already exists, it will be overwritten. |
CreateNew | Creates a new file. If the file already exists, an IOException is thrown. |
Open | Opens an existing file. If the file does not exist, a FileNotFoundException is thrown. |
OpenOrCreate | Opens an existing file. If the file does not exist, it creates a new file. |
Truncate | Opens an existing file and truncates it to a length of 0 bytes. If the file does not exist, a FileNotFoundException is thrown. |
FileInfo &fileinfo = *new FileInfo(S"file.dat"); FileStream *fs = fileinfo.Open(FileMode::Truncate); // or FileStream *fs = File::Open(S"file.dat", FileMode::CreateNew);
The second overload takes the additional parameter of the file access you require the file to have (see Table 8-6). The file will also be opened by default as unshared.
FILEACCESS | DESCRIPTION |
---|---|
Read | Allows data only to be read from the file |
ReadWrite | Allows data to be read from and written to the file |
Write | Allows data only to be written to the file |
FileInfo *fileinfo = new FileInfo(S"file.dat"); FileStream *fs = fileinfo->Open(FileMode::Truncate, FileAccess::ReadWrite); // or FileStream *fs = File::Open(S"file.dat", FileMode::Append, FileAccess::Write);
The final overload has one more parameter. It specifies how the file is shared with others trying to access it concurrently (see Table 8-7).
FILESHARE | DESCRIPTION |
---|---|
None | Specifies exclusive access to the current file. Subsequent openings of the file by a process, including the current one, will fail until the file closes. |
Read | Specifies that subsequent openings of the file by a process, including the current one, will succeed only if it is for a FileMode of Read. |
ReadWrite | Specifies that subsequent openings of the file by a process, including the current one, will succeed for either reading or writing. |
Write | Specifies that subsequent openings of the file by a process, including the current one, will succeed only if it is for a FileMode of Write. |
FileInfo *fileinfo = new FileInfo(S"file.dat"); FileStream *fs = fileinfo->Open(FileMode::Truncate, FileAccess::ReadWrite, FileShare::Read); // or FileStream *fs = File::Open(S"file.dat", FileMode::Append, FileAccess::Write, FileShare::None);
All those parameters make the file open process very configurable, but also a little tedious. This is especially true if you just want to open the file in a very generic and standard way. The .NET Framework class library provides you with a way to simplify file opening if the way you want to open a file happens to fall in one of six standard open configurations.
FileInfo &fileinfo = *new FileInfo(S"file.dat"); FileStream *CreateFile = fileinfo.Create(); FileStream *OpenReadFile = fileinfo.OpenRead(); FileStream *OpenWriteFile = fileinfo.OpenWrite(); StreamWriter *AppendTextFile = fileinfo.AppendText(); StreamWriter *CreateTextFile = fileinfo.CreateText(); StreamReader *OpenTextFile = fileinfo.OpenText(); // or FileStream *CreateFile = File::Create(S"file.dat"); FileStream *OpenReadFile = File::OpenRead(S"file.dat"); FileStream *OpenWriteFile = File::OpenWrite(S"file.dat"); StreamWriter *AppendTextFile = File::AppendText(S"file.dat"); StreamWriter *CreateTextFile = File::CreateText(S"file.dat"); StreamReader *OpenTextFile = File::OpenText(S"file.dat");
Notice that none of the preceding file opening methods takes any parameters, except the file path in the case of the static method of the File class. Personally, I think the names of the methods make them pretty self-explanatory.
Okay, you now have a file open and it is time to actually do something with it. Oops, did I say "file"? Files are only one thing that you can do I/O manipulation with. You can also do I/O manipulation in and out of memory using the MemoryStream and BufferedStream classes and in and out of network sockets using NetworkStream. You will look at the MemoryStream class a little later to see how it differs from a FileStream.
There are several different means to accomplish I/O manipulation. You will examine the three most common: using Streams, using TextReaders and TextWriters, and using BinaryReaders and BinaryWriters. Figure 8-3 shows the class hierarchy for manipulating files.
Figure 8-3: The class hierarchy for I/O manipulation
In the computer world, streams are a method of transferring blocks of data to and from one source to another in either a synchronous or asynchronous manner. The .NET Framework class library sends this data as a stream of bytes. A stream can also transfer these blocks of data starting from any location in one source to any location in another source.
What does this mean to you? Basically, you can read data, write data, and adjust the current location where you access the data. Not much to it, is there?
All stream-based I/O in the .NET Framework class library derives from the abstract base class Stream. The Stream class contains several virtual methods, which the inheriting class must define (see Table 8-8). Basically, these virtual methods define core Stream functionality and thus ensure that the inheriting class satisfies the definition of a stream as stated previously.
MEMBER | DESCRIPTION |
---|---|
CanRead | A Boolean value specifying whether reading is supported. |
CanSeek | A Boolean value specifying whether seeking is supported. |
CanWrite | A Boolean value specifying whether writing is supported. |
Close() | A method that closes the file and releases resources associated with the stream. |
Flush() | This method moves the data from the source buffer to its destination source and then clears the buffer. If the stream does not support a buffer, this method does nothing. |
Length | The length of the stream in bytes. |
Position | If seeking is supported, then this property can be used to get or set the position in the stream. |
Read() | Reads a specified number of bytes from the stream and then advances the position after the last read byte. |
ReadByte() | Reads a single byte from the stream and then advances the position after the byte. |
Seek() | If seeking is supported, then this method can be used to set the position in the stream. |
SetLength() | Sets the length of the stream in bytes. |
Write() | Writes a specified number of bytes to the stream and then advances the position after the last written byte. |
WriteByte() | Writes one byte to the stream and then advances the position after the byte. |
You will see some of these properties and methods implemented in the following stream implementations.
One of the most common implementations of a Stream is the FileStream class. This class provides implementations for the abstract Stream class so that it can perform file-based streaming. Or, in other words, it allows you to read from and write to a file.
You have already seen several ways to open a FileStream. It is also possible to open a FileStream directly without using File or FileInfo. To do this, you use one of the FileStream's many constructors. The most common parameters passed to the constructor are identical to those passed to the static File::Open() method.
FileStream *fs = new FileStream(S"file.dat", FileMode::CreateNew); FileStream *fs = new FileStream(S"file.dat", FileMode::Append, FileAccess::Write); FileStream *fs = new FileStream(S"file.dat", FileMode::Create, FileAccess: :Write, FileShare::None);
Once you finally have the FileStream open, you can start to read and/or write Bytes of data from or to it. As you saw from the virtual methods defined by the Stream class in Table 8-8, there are two ways of reading and writing to a stream. You can do it either by individual Bytes or by arrays of Bytes.
Byte data[] = { 'A', 'p', 'p', 'l', 'e' }; fso->Write(data, 0, 4); fso->WriteByte(data[4]); Byte ba[] = new Byte[5]; ba[0] = fsi->ReadByte(); fsi->Read(ba, 1, 4);
Simply placing the location in the Position property sets the location of the next place to read from or write to the file.
fsi->Position = 0;
You can also set the location of the next read or write by the Seek() method. This method allows you to use offsets from the beginning of the file (same as the Position property), the current location, or the end of the file.
fsi->Seek(0, SeekOrigin::Begin);
If you desire further access but want the data available in the file (for another operation or just for safety), flush the file buffer.
fso->Flush();
You should always close your files after you are done with them.
fso->Close();
Listing 8-3 shows the FileStream class in action and demonstrates many of the functionalities described previously.
Listing 8-3: Working with a FileStream
#using <mscorlib.dll> using namespace System; using namespace System::IO; Int32 main(void) { FileStream *fso = new FileStream(S"file.dat", FileMode::Create, FileAccess::Write, FileShare::None); Byte data[] = { 'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 't', 'e', 's', 't', '!', '\r', '\n', 'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'o', 'n', 'l', 'y', ' ', 'a', ' ', 't', 'e', 's', 't', '.', '\r', '\n' }; for (Int32 i = 0; i < data.Count-5; i += 5) { fso->Write(data, i, 5); } for (Int32 i = data.Count-4; i < data.Count; i++) { fso->WriteByte(data[i]); } fso->Close(); FileInfo *fi = new FileInfo(S"file.dat"); FileStream *fsi = fi->OpenRead(); Int32 b; while ((b = fsi->ReadByte()) != -1) { Console::Write((Char)b); } fsi->Position = 0; Byte ba[] = new Byte[17]; fsi->Read(ba, 0, 17); for (Int32 i = 0; i < ba.Count; i++) { Console::Write((Char)ba[i]); } Console::WriteLine(); fsi->Close(); // fi->Delete(); // If you want to get rid of it return 0; }
Figure 8-4 shows the file output generated by the FileStream.exe program.
Figure 8-4: File output of FileStream.exe
Programming with a MemoryStream is not much different from working with a FileStream. Obviously, what's happening behind the scenes, on the other hand, is completely different. You're no longer dealing with files; instead, you're dealing with computer memory.
There are only a few differences from a coding perspective when you deal with a MemoryStream. Obviously, the constructor is different.
MemoryStream *fs = new MemoryStream();
A MemoryStream has an additional property and a couple of unique methods (see Table 8-9).
MEMBER | DESCRIPTION |
---|---|
Capacity | This property gets or sets the number of bytes allocated to the stream. |
GetBuffer() | Returns an unsigned array of bytes that the stream created. |
WriteTo() | Writes the contents of the MemoryStream to another stream. This comes in handy if you want to write the stream out to a FileStream. |
Listing 8-4 shows the MemoryStream class in action and demonstrates many of the functionalities described previously.
Listing 8-4: Working with a MemoryStream
Int32 main(void) { Byte data[] = { 'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 't', 'e', 's', 't', '!', '\r', '\n', 'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'o', 'n', 'l', 'y', ' ', 'a', ' ', 't', 'e', 's', 't', '.','\r', '\n' }; MemoryStream *ms = new MemoryStream(); ms->Capacity = 40; for (Int32 i = 0; i < data.Count-5; i += 5) { ms->Write(data, i, 5); } for (Int32 i = data.Count-4; i < data.Count; i++) { ms->WriteByte(data[i]); } Byte ba[] = ms->GetBuffer(); for (Int32 i = 0; i < ba.Count; i++) { Console::Write((Char)ba[i]); } Console::WriteLine(S""); FileStream *fs = File::OpenWrite(S"file.dat"); ms->WriteTo(fs); fs->Close(); ms->Close(); return 0; }
Figure 8-5 shows a display of the buffer contained within the MemoryStream. Figure 8-6 shows the results displayed to the console. Figure 8-7 shows the resulting file output generated by the MemoryStream.exe program. Notice that Figures 8-5 through 8-7 all have the same results, as expected.
Figure 8-5: Display of the buffer of the MemoryStream created by MemoryStream.exe
Figure 8-6: Console results of MemoryStream.exe
Figure 8-7: File output of MemoryStream.exe
A drawback when using a FileStream is that it isn't very String- or character-friendly. Because what you often want to store are Strings and characters, it only makes sense that methods be made to optimize and simplify the process of writing these to a stream. This is where the StreamReader and StreamWriter classes become helpful.
Just like the Stream class, the abstract StreamReader and StreamWriter classes define all the functionality that needs to be implemented to support String and character reading and writing (see Tables 8-10 and 8-11).
METHOD | DESCRIPTION |
---|---|
Close() | Closes the file and releases any resources |
Peek() | Reads the next character without advancing the stream pointer |
Read() | Reads data from the input stream |
ReadBlock() | Reads a specified number of characters from the stream to a specified starting location in an input buffer |
ReadLine() | Reads a line of data from the input stream and returns it as a String |
ReadToEnd() | Reads the rest of the data from the current file location to the end and returns it as a single String |
METHOD | DESCRIPTION |
---|---|
Close() | Closes the file and releases any resources |
Flush() | Forces the writing of the current buffer and then clears it |
Write() | Writes the specified String to the output stream |
WriteLine() | Writes the specified String to the output stream, then writes the NewLine String |
There are many ways to create a StreamReader and a StreamWriter. You can start from the File or FileInfo class and create one directly from its methods. It is also possible to build one from a FileStream, again using the File or FileInfo class or with the FileStream constructor.
StreamReader *sr1 = File::OpenText(S"file.dat"); StreamWriter *sw1 = fileinfo->CreateText(S"file.dat"); StreamReader *sr2 = new StreamReader(File::Open(S"-file.dat", FileMode::Open, FileAccess::Read, FileShare::None)); StreamWriter *sw2 = new StreamWriter(new FileStream(S"file.dat", FileMode::Create, FileAccess::Write, FileShare::None));
Writing to the StreamWriter, after you have created it, is no different than writing to the console. You should be very familiar with the Write() and WriteLine() methods. Reading is a little trickier, as you can read one character, an array of characters, or the rest of the characters in the stream. In most cases, you will most likely be using the StreamReader methods ReadLine() and ReadToEnd(). The first reads a single line of text while the second reads all the text remaining on the stream. Both return their results as a String.
String *in1 = sr->ReadLine(); String *in2 = sr->ReadToEnd();
Listing 8-5 shows the StreamWriter and StreamReader classes in action and demonstrates many of the functionalities described previously. It also resembles the previous examples but, as you can see, the code is much simpler.
Listing 8-5: Working with a StreamWriter and a StreamReader
Int32 main(void) { String *data[] = { S"This is ", S"a test!", S"This is only a test." }; StreamWriter *sw = new StreamWriter(new FileStream(S"-file.dat", FileMode::Create, FileAccess::Write, FileShare::None)); for (Int32 i = 0; i < data.Count-1; i++) { sw->Write(data[i]); } sw->WriteLine(S""); sw->WriteLine(data[2]); sw->Close(); StreamReader *sr = File::OpenText(S"file.dat"); String *in = sr->ReadLine(); Console::WriteLine(in); Console::WriteLine(sr->ReadToEnd()); sw->Close(); return 0; }
Figure 8-8 shows the results of StreamRW.exe displayed to the console. Figure 8-9 shows the resulting file output generated by the StreamRW.exe program. Notice that Figures 8-8 and 8-9 have the same results, as expected.
Figure 8-8: Console results of StreamRW.exe
Figure 8-9: File output of StreamRW.exe
You have looked at I/O for Bytes and Strings. What if you want to store all the other data types, such as Booleans, integers, and floating points? This is where the BinaryReader and BinaryWriter come into play. These classes were designed specifically to handle all the .NET Framework's built-in data types (including Byte and String).
To create a BinaryReader or BinaryWriter class, you need to use its constructor and pass it a Stream. This means, by the way, that BinaryReaders and BinaryWriters can take as a parameter a FileStream, MemoryStream, NetworkStream, and so on.
FileStream *fs = File::OpenRead(fname); BinaryReader *br = new BinaryReader(fs); MemoryStream *ms = new MemoryStream(); BinaryWriter *br = new BinaryWriter(ms);
The process of writing with the BinaryWriter is very simple. After you create your BinaryWriter, you only need to use two more methods, Write() and Close(). The Write() method takes care of all the hard work by being made up of numerous overloaded versions of itself (one for each supported data type).
The BinaryReader class is a little harder to work with. This time, you need to work with many different read methods (one for each supported type). They all have the same syntax: Readxxx(), where xxx is the data type. Examples of read methods are ReadInt32(), ReadBoolean(), and ReadSingle().
A drawback of the BinaryReader is that you need to know the data type you are reading in before you actually do the read, so that you can make the correct call.
Listing 8-6 shows the BinaryWriter and BinaryReader classes in action and demonstrates many of the functionalities described previously. You might want to notice the special coding you need to be do to handle DateTime classes.
Listing 8-6: Working with a BinaryWriter and a BinaryReader
__gc class Player { String *Name; Int32 Strength; Boolean IsMale; public: Player() {} Player (String *Name, Int32 Str, Boolean IsMale) { this->Name = Name; this->Strength = Str; this->IsMale = IsMale; } void Print() { Console::WriteLine(S"Name: {0} ({1})", Name, (IsMale ? S"M" : S"F")); Console::WriteLine(S"Str: {0}", __box(Strength)); Console::WriteLine(S"Date: {0}", CreateDate.ToString()); } void Save(String *fname) { FileStream *fs = File::OpenWrite(fname); BinaryWriter *bw = new BinaryWriter(fs); bw->Write(Name); bw->Write(Strength); bw->Write(IsMale); // Due to multiculture this is a safe way of storing DateTimes bw->Write(CreateDate.Ticks); bw->Close(); fs->Close(); } void Load(String *fname) { FileStream *fs = File::OpenRead(fname); BinaryReader *br = new BinaryReader(fs); Name = br->ReadString(); Strength = br->ReadInt32(); IsMale = br->ReadBoolean(); // Due to multicultures this is a safe way of retrieving DateTimes CreateDate = DateTime( br->ReadInt64() ); br->Close(); fs->Close(); } }; Int32 main(void) { Player *Joe = new Player(S"Joe", 10, true); Joe->Save(S"Player.dat"); Console::WriteLine(S"Original Joe"); Joe->Print(); Player *JoeClone = new Player(); JoeClone->Load(S"Player.dat"); Console::WriteLine(S"\nCloned Joe"); JoeClone->Print(); return 0; }
Figure 8-10 shows the results of BinaryRW.exe displayed to the console. Figure 8-11 shows the resulting file output generated by the BinaryRW.exe program. Notice that Figure 8-11 is pretty unreadable unless you know the format in which it was stored. The fact that Figure 8-10 and Figure 8-11 represent the same data is not obvious.
Figure 8-10: Console results of BinaryRW.exe
Figure 8-11: File output of BinaryRW.exe