Flylib.com

Books Software

 
 
 

Microsoft Visual C# 2005 Unleashed - page 36


Summary

This chapter has given you a glimpse at the incredible power that a developer can wield using generics. Generics are a tool that allow developers to maximize performance and code reuse as well as create a robust object-oriented programming environment by not having to resort to repetitive typecasting and the overuse of parameters of type System.Object . Using generics, developers can defer the specification of data types until such time as their class is instantiated , but still reap the benefits of strong data types. After reading this chapter, you should be able to continue learning all that C# 2.0 has to offer and feel comfortable when you see generics being used by the .NET Framework and in your own sample code.



Part II: .NET Framework 2.0 Fundamentals

In This Part
 

CHAPTER 7 I/O and Persistence 77

 

CHAPTER 8 Working with XML 91

 

CHAPTER 9 Events and Delegates 103

 

CHAPTER 10 Multithreaded Programming 121

 

CHAPTER 11 Reflection Fundamentals 141

 

CHAPTER 12 Assemblies and AppDomains 155

 

CHAPTER 13 COM and Windows Interoperability 169

 

CHAPTER 14 Code Access Security 181

 

CHAPTER 15 Cryptography and Data Protection 193

 

CHAPTER 16 Optimizing Your .NET 2.0 Code 207



Chapter 7. I/O and Persistence

In This Chapter

  • Introduction to Streams

  • Introduction to Basic File I/O

  • Using Asynchronous File I/O

  • Working with Isolated Storage

In this day and age of enterprise applications and smart clients that get their data from large relational databases like SQL Server and Oracle, it is often easy to overlook the fact that we still use the underlying file system for something other than storing shortcuts and documents.

We use files for numerous tasks , including storing configuration settings, images, icons, textual data (such as comma-delimited files from other sources like mainframes), file-based databases such as Microsoft Access, Excel spreadsheets, and even XML data.

This chapter shows you the basics of working with files using the .NET Framework, including how to work with streams, the basic unit of input/output (I/O) used throughout the entire framework. Finally, you'll see some more advanced file I/O techniques such as asynchronous I/O and the use of isolated storage to provide a secure, isolated location for your application's data.



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.