One of the wrinkles that the .NET Framework brings to the developer’s table is a new IO system built around streams. It’s new, that is, for VB programmers, who have likely never dealt with streams outside of the FileSystemObject. Other languages have had stream IO for years. Let’s take a closer look at .NET’s support for streams.
What is a stream, and why do I care?
A stream is an abstract way of sending and receiving data. When dealing with a stream, you need to concentrate only on the endpoints—pulling data from, and pushing data into, the stream. You don’t need to be concerned for what’s at the other end of the stream, how data is delivered to the stream’s other endpoint, or even how it’s formatted when it gets there. Streams give you incredible flexibility in your applications, allowing you to read from and write to memory, the console, or even a network connection as if it were a simple disk file.
The .NET Framework has many flavors of stream to suit different circumstances. Table A lists the major ones. They all descend from the abstract System.IO.Stream class, which itself provides support for basic IO functions like reading, writing, and changing your current position in the stream, also known as seeking. Stream also provides support for synchronous and asynchronous operations, allowing you to perform lengthy IO operations in the background.
Care and feeding of your stream
Unless you cut your teeth on VB’s archaic file access statements, you’ll find reading from and writing to a stream is fairly straightforward. Listing A illustrates reading to and writing from a hypothetical text file using FileStream. All Stream objects support read and write operations via their Read and Write methods. However, these methods accept only bytes and byte arrays. Sometimes, that’s exactly what you want. But when you’re dealing with a simple text file, like my example in Listing A, this forces you into a little extra work—as you can see by my use of the static System.Text.Encoding.ASCII methods.
The other thing you might notice in the example is that there is really no way to read an entire array from a stream or to write an entire array. You must instead use the offset and length parameters to specify a beginning point and the number of bytes to read from or write to the stream. This makes it possible to overrun a stream and attempt to pull more bytes from it than it has available. But this isn’t really a problem, as any excess array elements will simply be empty after your Read completes.
Not quite Synchronicity
The methods we’ve discussed so far are synchronous—execution will stop on a call to any of them until the method call returns. That can be inconvenient if you are conducting a long read or write operation. Fortunately, .NET streams also support asynchronous read and write operations through the BeginRead, EndRead, BeginWrite, and EndWrite methods.
To start an asynchronous IO operation on a stream, call the appropriate Begin… method, passing it a callback delegate. The operation will then run on a background thread, and your delegate member will be called when the operation is completed. Check out Listing B for an example of an asynchronous read operation. You can also suspend the current thread until the operation completes by calling the appropriate End… method. The call will block until the Begin… method returns.
Row, row, row your boat
I know, some of you are by now saying, “Lamont, I’m interested in coding for text data, and this byte array business stinks—really bad.” And I agree with you. Fortunately, .NET supports the concept of a reader object that can wrap itself around a stream and convert more complex data types to and from the byte values that Stream descendents understand.
The StreamReader and StreamWriter objects are both able to deal with text data and handle any necessary conversions internally. For other primitive data types, the BinaryReader and BinaryWriter classes are useful.
To extend our little example of text file access a bit, Listing C accomplishes the same task as Listing A but uses a StreamReader and a StreamWriter to wrap access to the FileStream. You can see that this simplifies the code somewhat, but you are still forced to specify a starting offset and maximum length of characters to read or write.
Now for the interesting part: The reader and writer objects can be used with any descendant of Stream. Why is this interesting, you ask? Well, your code has more or less complete independence from the underlying type of stream, and therefore, from the source of the data. A StreamReader or BinaryReader works in the same way whether it’s reading from a FileStream or from a NetworkStream. Ah, the beauty of object-oriented programming.
The only thing to watch out for is that readers and writers are not thread safe by default. So when sharing a reader or writer object on multiple threads, be sure to use the static TextReader.Synchronized method to get a thread-safe wrapper object to use instead.
That’ll about do it for our introduction to the wonderful world of streams. I’ll introduce some more practical uses for them in a future article. Until then, remember to wear your life preserver and watch out for the piranhas.