Introduction to
Streams
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.
Table 7.1.
Stream
Properties
|
Property
|
Description
|
|
CanRead
|
This property is used by deriving classes (such
as
MemoryStream
) to
indicate
whether the stream supports
read operations.
|
|
CanWrite
|
Indicates whether the stream supports write
operations.
|
|
CanSeek
|
Indicates whether the stream supports seek
operations. Some streams are forward-only and do not allow seeking
to specific
positions
.
|
|
Length
|
Indicates the length, or size, of the stream in
bytes.
|
|
Position
|
Indicates the current pointer position of the
stream.
|
Table 7.2.
Stream
Methods
|
Method
|
Description
|
|
BeginRead
|
Starts an asynchronous (multithreaded) read
operation
|
|
BeginWrite
|
Starts an asynchronous write operation
|
|
Close
|
Closes the current stream and releases
associated resources (such as underlying database resources,
network sockets, OS-level file handles, and so on)
|
|
EndRead
|
Completes an asynchronous read operation
|
|
EndWrite
|
Completes an asynchronous write operation
|
|
Flush
|
Clears any buffers in the stream and stores any
uncommitted data in the underlying backing store
|
|
Read
|
Reads an array of bytes from the stream
|
|
ReadByte
|
Reads a single byte from the stream
|
|
Seek
|
Moves the pointer to the indicated position,
relative to the beginning, end, or current position of the
stream
|
|
SetLength
|
Expands or contracts the stream, if
supported
|
|
Write
|
Writes an array of bytes to the stream
|
|
WriteByte
|
Writes a single byte to the stream
|
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
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace MemStream
{
class Program
{
static void Main(string[] args)
{
string sourceString = "Mary had a little lamb.";
MemoryStream ms = new MemoryStream(100) ;
// put some data on the stream
ms.Write(ASCIIEncoding.ASCII.GetBytes(sourceString),
0, sourceString.Length);
Console.WriteLine("After initial write:");
Console.WriteLine(string.Format(
"Capacity: {0}\nLength: {1}\nPosition: {2}",
ms.Capacity, ms.Length, ms.Position));
ms.Seek(0, SeekOrigin.Begin);
// read the first 4 bytes of the stream
byte[] tempBytes = new byte[sourceString.Length];
ms.Read(tempBytes, 0, 4);
Console.WriteLine(ASCIIEncoding.ASCII.GetString(tempBytes));
// get the word 'lamb'
ms.Seek(-5, SeekOrigin.End);
ms.Read(tempBytes, 0, 4) ;
Console.WriteLine(ASCIIEncoding.ASCII.GetString(tempBytes));
// write some bytes
ms.Seek(11, SeekOrigin.Begin);
ms.Write(ASCIIEncoding.ASCII.GetBytes("really"),0, 6);
// now get the whole stream
ms.Seek(0, SeekOrigin.Begin);
ms.Read(tempBytes, 0, (int)ms.Length);
Console.WriteLine(ASCIIEncoding.ASCII.GetString(tempBytes));
ms.Close();
Console.ReadLine();
}
}
}
|
{% if main.adsdop %}{% include 'adsenceinline.tpl' %}{% endif %}
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.
|