The System.IO types serve as the primary means for stream-oriented I/Ofiles, principally, although the abstract types defined here serve as base classes for other forms of I/O, such as the XML stack in System.Xml. The System.IO namespace is shown in Figure 31-1 and Figure 31-2.
The System.IO namespace can be seen as two distinct partitions: a set of utility types for using and working with the local machine's filesystem, and a protocol stack for working with bytestream-oriented input and output. The former partition is the collection of classes such as Directory and FileSystemWatcher, whereas the latter partition is the set of Stream and Reader/Writer types.
The Stream types in System.IO follow a basic object model, similar to the I/O model used by the C/C++ runtime library: all serial byte access is a stream, and there are different sources and sinks for this serialized byte data. In the System.IO package, this is represented directly by the abstract base type Stream; its concrete subtypes represent the actual I/O access: FileStream represents I/O to a file, and MemoryStream represents I/O to a literal array of bytes (whose size is dynamically managed) in memory. Other packages within the .NET Framework Class Library offer up their own Stream-derived types. For example, in the System.Net namespace, socket connections and HTTP responses are offered up as Stream-derived types, giving .NET programmers the ability to treat any sort of input or output data as "just a stream."
Simply reading and writing to these streams is not enough of an abstraction, however. In particular, programmers often need to perform one of two sorts of I/O: binary I/O, which is writing actual binary representations of objects or data to disk, or text I/O, which is writing the textual representations of that data. These operations are fundamentally differentwriting the text representation of the integer value 5 produces the literal text "5" within the stream, whereas writing the binary value generates the hex value 0x00000005 (represented as four bytes, 05 00 00 00, in the file). In the .NET libraries, because these types of I/O operations are different from one another, these operations are abstracted out into two sets of abstract base types. BinaryReader and BinaryWriter are for reading and writing binary values to streams, and TextReader and TextWriter are for reading and writing character-based data.
Note that the System.IO namespace also offers some interesting stream-on-stream options. Like the Java java.io package, Stream types can layer on top of other Stream types to offer additional functionalitythis is the Decorator pattern (from the Design Patterns book). The sole example of this in the System.IO namespace is the BufferedStream, which maintains a buffer on top of the Stream object passed to it in its constructor.
All of these types work together to provide some powerful abstraction and composite behaviors. For example, when working with random-access data, create a BinaryReader around a BufferedStream, which in turn wraps around a FileStream. If you decide later to store the random-access data in memory for optimization's sake, change the BufferedStream/FileStream pair to a MemoryStream. When reading a configuration file, choose to declare the ReadConfiguration method you have written to take an arbitrary TextReader, rather than ask for a string containing the filename. This allows for flexibility laterperhaps the configuration wants to be stored into a CLOB field in an RDBMS. Simply change the actual Stream instance passed into the TextReader, and start reading the configuration out of the RDBMS, off of a socket request, or out of the HTTP response sent to a web server. Similarly, when planning to extend the System.IO namespace's capabilities, try to follow this same model. If you want to add compression to save on a configuration file's size, just build a CompressingStream that wraps another Stream in the manner BufferedStream does. If you want to have some interprocess communication with an existing "legacy" Win32 app (perhaps communicate over an NT Named Pipe), simply build a NamedPipeStream. In general, there is no particular reason to take specific derivatives of Stream as parametersby limiting expected parameters to be of type Stream, .NET programmers can gain an incredible amount of flexibility regarding where and how data lives.
All this notwithstanding, certain programmatic tasks simply require access to the filesystem. The underlying filesystem is a hierarchical one, and there will be times there is simply no escaping that fact. For these tasks, the .NET System.IO namespace provides the filesystem types: Directory, DirectoryInfo, File, FileInfo and its associated enumerations, FileSystemInfo, FileSystemWatcher, and Path (finally, a class that understands directory paths in all their various incarnations!). These classes should be used for mostly "meta-file" operations (enumerating files, discovering attributes about a file, creating or destroying a directory, and so on) rather than for operations on the contents of the file (for which the Stream-based types described earlier are more appropriate).