A stream is an object that essentially connects data between two endpoints with a narrow access window. Streams allow you to write and read small portions of data at a time, providing for an extremely efficient means of access. For example, using a stream, you can read data in small portions from a file that is several hundred megabytes in size without actually consuming several hundred megabytes of memory. The same is true of writing to streams. You can place small amounts of data on a stream without having to have all of the data in memory at any given time.
In addition to the performance benefit, streams also provide a unified model for reading and writing data, regardless of the format or location of the underlying data. For example, you can access data from a disk file using a stream and you can access data from a relational database or from a web service in a stream as well.
Streams can also be connected through a process referred to as composition. By composing streams, you can attach various types of reader and writer classes to the end of the stream to make data access easier. As you will see in Chapter 15 "Cryptography and Data Protection," you can even attach specially encrypted streams so that data is encrypted as soon as it is placed on the stream. Streams can also be used for network communication in addition to file I/O.
Using Memory Streams
Streams are the basic unit of I/O in the .NET Framework and you will find them used everywhere. Before getting into working with physical files on disk, this section will illustrate the basics of opening streams and reading and writing from streams using the MemoryStream class as an example. When you know how to manipulate a MemoryStream, you will find that you will be able to use all of the other types of streams exposed by .NET Framework classes with little difficulty.
As mentioned earlier, streams provide a narrow window of access. This often causes developers trouble. For example, when you write to a stream, the Position of the pointer in the stream advances. When you read from a stream, the read always starts from the current pointer position within the stream. A common source of problems when reading from streams is not setting the pointer position properly with the Seek method.
Before taking a look at the MemoryStream sample code, take a look at Tables 7.1 and 7.2, which list the methods and properties that belong to all Stream classes, regardless of the underlying data store.
The code shown in Listing 7.1 illustrates how to instantiate a stream, as well as how to read and write information from that stream.
Listing 7.1. MemoryStream Sample
Before running this code, see if you can predict the output. The code that retrieves the first four bytes of the stream should be fairly obvious; the output will be the word Mary.
Working with Unicode
When working with streams that contain textual data, knowing the size of your characters is paramount. For example, when working with traditional ASCII characters, the size of each character is exactly one byte. This makes math easy. However, when working with Unicode when dealing with foreign languages, you need to remember that each character requires two bytes. Therefore, when you convert byte arrays into strings, you need to use the UnicodeEncoding class, and you need to allocate twice the number of characters in bytes when initializing your byte arrays.
Next, the code seeks to a position five bytes before the end of the stream and then grabs the next four bytes, producing the word "lamb."
The next few lines of code are typically where a lot of developers get confused. When you write to a stream, you overwrite whatever bytes might lie underneath. So, when writing "really" to the stream, you don't get "Mary had a really little lamb." as one might expect. Rather, your stream contains "Mary had a really lamb.". If your goal is truly to insert data at a certain point in the stream, and have the remainder of the stream remain intact, you will need to do it the "old-fashioned way." This involves using two streams and copying the old into the new, making sure to insert the new data in the right place in the new stream.