In the first half of the chapter, you learned how to manipulate files and folders. Now we’ll discuss how to access files (write data to files and read it back). You’ve already seen the various Open methods of the File class, which return a Stream object. It’s this object that provides the methods for writing to and reading from files.
There are two types of files: text files and binary files. Of course, you can classify files in any way you like, but when it comes to writing to and reading from files, it’s convenient to treat them as either text or binary. A binary file is any file that doesn’t contain plain text. Text files are usually read line by line or in their entirety into a String variable. Binary files must be read according to the type of information stored in them. A bitmap file, for instance, must be read 1 byte at a time. Each pixel is usually represented by 3 or 4 bytes, and you must combine the values read to reconstruct the pixel’s color. You can also read Long values from an image file. Or you can read a Color variable directly from the stream. Most binary files contain multiple data types, and you must know the organization of a file before you can read it.
To access a file, you must first set up a Stream object. Stream objects are created by the various methods that open or create files, as you have seen in the previous sections, and they return information about the file they’re connected to.
After the Stream object is in place, you create a Reader or Writer object, which enables you to read information from or write information into the Stream, respectively. The Reader and Writer classes are abstracts that you can’t use directly in your code. There are two classes that inherit from the Reader class: the StreamReader class for text files and the BinaryReader class for binary files. Likewise, there are two classes that inherit the Writer class: the StreamWriter and the BinaryWriter classes. These classes expose a few properties and methods for writing to files and reading from them, and their members are discussed shortly.
Using Streams, Readers, and Writers
The FileStream class is derived from the Stream abstract class and represents a stream of bytes. Use its methods to find the length of the stream, to lock the stream, and to navigate to a specific location in the stream. FileStream also provides methods of writing to the stream and reading from it. The Write and WriteByte methods write an array of bytes and a single byte to the stream, respectively. The Read and ReadByte methods read the same data back from the stream. These methods are not used frequently, because developers usually manipulate more-specific data types (such as strings and decimals) or custom data types, not bytes. If you’re dealing with bytes, or you don’t mind converting your data to and from Byte arrays, use the methods of the FileStream class to write data to a file. For most applications, however, you’ll use the methods of the Stream class.
Typical Windows applications set up a FileStream object to open a channel between the application and a file, and then a StreamWriter/StreamReader object on top of it. These two classes provide more-flexible methods for sending data to the underlying file and reading them back. For binary files, use the BinaryWriter/BinaryReader classes. The Reader/Writer classes know how to send data to a Stream object and read them back, while the Stream object knows how to interact with the underlying file. The IO namespace provides many ways to exchange data with a file and you’ll rarely use the FileStream class’s methods to write or read data directly to or from a file.
You can think of the Stream as a channel between your application and the source or destination of the data. In most cases, the source (or destination) is a file. The Stream abstracts a very basic operation: the operation of sending or receiving data. Does it really make any difference whether you write to a file or send data to a web client? Technically, it’s a world of difference, but wouldn’t it be nice if we could specify the destination and then send the data (or request data from a source)? The Framework abstracts this basic operation by establishing a Stream between the application and the source, or destination, of the data. Consider an application that successfully writes data to a file. If you change the definition of the stream, you can send the same data to a different destination via the same statements. You can send the data to another machine or a web client.
Another benefit of using streams is that you can combine them. The typical example is that of encrypting and decrypting data. Data is encrypted through a special type of Stream, the CryptoStream. You write plain data to the CryptoStream, and they’re encrypted by the stream itself. In other words, the CryptoStream object accepts plain data and emits encrypted data. You can connect the CryptoStream object to another Stream object that represents a file and write the encrypted data directly to the file. Your code uses simple statements to write data to the CryptoStream object, which encrypts the data and then passes it to another stream that writes the encrypted data to a file.