.NET Framework Data Types

The C in FCL stands for 鈥渃lass,鈥?but the FCL isn鈥檛 strictly a class library; it鈥檚 a library of types. Types can mean any of the following:

Understanding what a type is and how one type differs from another is crucial to understanding the FCL. The information in the next several sections will not only enrich your understanding of the FCL, but also help you when the time comes to build data types of your own.

Classes

A class in the .NET Framework is similar to a class in C++: a bundle of code and data that is instantiated to form objects. Classes in traditional object-oriented programming languages such as C++ contain member variables and member functions. Framework classes are richer and can contain the following members:

Here, in C#, is a class that implements a Rectangle data type:

class聽Rectangle
{
聽聽聽聽//聽Fields
聽聽聽聽protected聽int聽width聽=聽1;
聽聽聽聽protected聽int聽height聽=聽1;

聽聽聽聽//聽Properties
聽聽聽聽public聽int聽Width
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽width;聽}
聽聽聽聽聽聽聽聽set
聽聽聽聽聽聽聽聽{
聽聽聽聽聽聽聽聽聽聽聽聽if聽(value聽>聽0)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽width聽=聽value;
聽聽聽聽聽聽聽聽聽聽聽聽else
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽throw聽new聽ArgumentOutOfRangeException聽(
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽 "Width聽must聽be聽1聽or聽higher");
聽聽聽聽聽聽聽聽}
聽聽聽聽}

聽聽聽聽public聽int聽Height
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽height;聽}
聽聽聽聽聽聽聽聽set
聽聽聽聽聽聽聽聽{
聽聽聽聽聽聽聽聽聽聽聽聽if聽(value聽>聽0)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽height聽=聽value;
聽聽聽聽聽聽聽聽聽聽聽聽else
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽throw聽new聽ArgumentOutOfRangeException聽(
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽 "Height聽must聽be聽1聽or聽higher");
聽聽聽聽聽聽聽聽}
聽聽聽聽}

聽聽聽聽public聽int聽Area
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽width聽*聽height;聽}
聽聽聽聽}

聽聽聽聽//聽Methods聽(constructors)
聽聽聽聽public聽Rectangle聽()聽{}
聽聽聽聽public聽Rectangle聽(int聽cx,聽int聽cy)
聽聽聽聽{
聽聽聽聽聽聽聽聽Width聽=聽cx;
聽聽聽聽聽聽聽聽Height聽=聽cy;
聽聽聽聽}
}

Rectangle has seven class members: two fields, three properties, and two methods, which both happen to be constructors鈥攕pecial methods that are called each time an instance of the class is created. The fields are protected, which means that only Rectangle and Rectangle derivatives can access them. To read or write a Rectangle object鈥檚 width and height, a client must use the Width and Height properties. Notice that these properties鈥?set accessors throw an exception if an illegal value is entered, a protection that couldn鈥檛 be afforded had Rectangle鈥檚 width and height been exposed through publicly declared fields. Area is a read-only property because it lacks a set accessor. A compiler will flag attempts to write to the Area property with compilation errors.

Many languages that target the .NET Framework feature a new operator for instantiating objects. The following statements create instances of Rectangle in C#:

Rectangle聽rect聽=聽new聽Rectangle聽();聽聽聽聽聽//聽Use聽first聽constructor
Rectangle聽rect聽=聽new聽Rectangle聽(3,聽4);聽//聽Use聽second聽constructor

Once the object is created, it might be used like this:

rect.Width聽*=聽2;聽聽聽聽聽聽//聽Double聽the聽rectangle's聽width
int聽area聽=聽rect.Area;聽//聽Get聽the聽rectangle's聽new聽area

Significantly, neither C# nor any other .NET programming language has a delete operator. You create objects, but the garbage collector deletes them.

In C#, classes define reference types, which are allocated on the garbage-collected heap (which is often called the managed heap because it鈥檚 managed by the garbage collector) and accessed through references that abstract underlying pointers. The counterpart to the reference type is the value type, which you鈥檒l learn about in the next section. Most of the time you don鈥檛 have to be concerned about the differences between the two, but occasionally the differences become very important and can actually be debilitating to your code if not accounted for. See the section 鈥淏oxing and Unboxing鈥?later in this chapter for details.

All classes inherit a virtual method named Finalize from System.Object, which is the ultimate root class for all data types. Finalize is called just before an object is destroyed by the garbage collector. The garbage collector frees the object鈥檚 memory, but classes that wrap file handles, window handles, and other unmanaged resources (鈥渦nmanaged鈥?because they鈥檙e not freed by the garbage collector) must override Finalize and use it to free those resources. This, too, has some important implications for developers. I鈥檒l say more later in this chapter in the section entitled 鈥淣ondeterministic Destruction.鈥?/p>

Incidentally, classes can derive from at most one other class, but they can derive from one class and any number of interfaces. When you read the documentation for FCL classes, don鈥檛 be surprised if you occasionally see long lists of base 鈥渃lasses,鈥?which really aren鈥檛 classes at all, but interfaces. Also be aware that if you don鈥檛 specify a base class when declaring a class, your class derives implicitly from System.Object. Consequently, you can call ToString and other System.Object methods on any object.

Structs

Classes are intended to represent complex data types. Because class instances are allocated on the managed heap, some overhead is associated with creating and destroying them. Some types, however, are 鈥渟imple鈥?types that would benefit from being created on the stack, which lives outside the purview of the garbage collector and offers a high-performance alternative to the managed heap. Bytes and integers are examples of simple data types.

That鈥檚 why the .NET Framework supports value types as well as reference types. In C#, value types are defined with the struct keyword. Value types impose less overhead than reference types because they鈥檙e allocated on the stack, not the heap. Bytes, integers, and most of the other 鈥減rimitive鈥?data types that the CLR supports are value types.

Here鈥檚 an example of a simple value type:

struct聽Point
{
聽聽聽聽public聽int聽x;
聽聽聽聽public聽int聽y;
聽聽聽聽public聽Point聽(int聽x,聽int聽y)
聽聽聽聽{
聽聽聽聽聽聽聽聽this.x聽=聽x;
聽聽聽聽聽聽聽聽this.y聽=聽y;
聽聽聽聽}
}

Point stores x and y coordinates in fields exposed directly to clients. It also defines a constructor that can be used to instantiate and initialize a Point in one operation. A Point can be instantiated in any of the following ways:

Point聽point聽=聽new聽Point聽(3,聽4);聽//聽x==3,聽y==4
Point聽point聽=聽new聽Point聽();聽聽聽聽聽//聽x==0,聽y==0
Point聽point;聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽//聽x==0,聽y==0

Note that even though the first two statements appear to create a Point object on the heap, in reality the object is created on the stack. If you come from a C++ heritage, get over the notion that new always allocates memory on the heap. Also, despite the fact that the third statement creates a Point object whose fields hold zeros, C# considers the Point to be uninitialized and won鈥檛 let you use it until you explicitly assign values to x and y.

Value types are subject to some restrictions that reference types are not. Value types can鈥檛 derive from other types, although they implicitly derive from System.ValueType and can (and often do) derive from interfaces. They also shouldn鈥檛 wrap unmanaged resources such as file handles because value types have no way to release those resources when they鈥檙e destroyed. Even though value types inherit a Finalize method from System.Object, Finalize is never called because the garbage collector ignores objects created on the stack.

Interfaces

An interface is a group of zero or more abstract methods鈥攎ethods that have no default implementation but that are to be implemented in a class or struct. Interfaces can also include properties and events, although methods are far more common.

An interface defines a contract between a type and users of that type. For example, many of the classes in the System.Collections namespace derive from an interface named IEnumerable. IEnumerable defines methods for iterating over the items in a collection. It鈥檚 because the FCL鈥檚 collection classes implement IEnumerable that C#鈥檚 foreach keyword can be used with them. At run time, the code generated from foreach uses IEnumerable鈥檚 GetEnumerator method to iterate over the collection鈥檚 contents.

Interfaces are defined with C#鈥檚 interface keyword:

interface聽ISecret
{
聽聽聽聽void聽Encrypt聽(byte[]聽inbuf,聽out聽byte[]聽outbuf,聽Key聽key);
聽聽聽聽void聽Unencrypt聽(byte[]聽inbuf,聽out聽byte[]聽outbuf,聽Key聽key);
}

A class or struct that wants to implement an interface simply derives from it and provides concrete implementations of its methods:

class聽Message聽:聽ISecret
{
聽聽聽聽public聽void聽Encrypt聽(byte[]聽inbuf,聽out聽byte[]聽outbuf,聽Key聽key)
聽聽聽聽{
聽聽聽聽聽聽...
聽聽聽聽}

聽聽聽聽public聽void聽Unencrypt聽(byte[]聽inbuf,聽out聽byte[]聽outbuf,聽Key聽key)
聽聽聽聽{
聽聽聽聽聽聽...
聽聽聽聽}
}

In C#, the is keyword can be used to determine whether an object implements a given interface. If msg is an object that implements ISecret, then in this example, is returns true; otherwise, it returns false:

if聽(msg聽is聽ISecret)聽{
聽聽聽聽ISecret聽secret聽=聽(ISecret)聽msg;
聽聽聽聽secret.Encrypt聽(...);
}

The related as operator can be used to test an object for an interface and cast it to the interface type with a single statement.

Enumerations

Enumerations in .NET Framework鈥搇and are similar to enumerations in C++. They鈥檙e types that consist of a set of named constants, and in C# they鈥檙e defined with the enum keyword. Here鈥檚 a simple enumerated type named Color:

enum聽Color
{
聽聽聽聽Red,
聽聽聽聽Green,
聽聽聽聽Blue
}

With Color thusly defined, colors can be represented this way:

Color.Red聽聽聽聽//聽Red
Color.Green聽聽//聽Green
Color.Blue聽聽聽//聽Blue

Many FCL classes use enumerated types as method parameters. For example, if you use the Regex class to parse text and want the parsing to be case-insensitive, you don鈥檛 pass a numeric value to Regex鈥檚 constructor; you pass a member of an enumerated type named RegexOptions:

Regex聽regex聽=聽new聽Regex聽(exp,聽RegexOptions.IgnoreCase);

Using words rather than numbers makes your code more readable. Nevertheless, because an enumerated type鈥檚 members are assigned numeric values (by default, 0 for the first member, 1 for the second, and so on), you can always use a number in place of a member name if you prefer.

The enum keyword isn鈥檛 simply a compiler keyword; it creates a bona fide type that implicitly derives from System.Enum. System.Enum defines methods that you can use to do some interesting things with enumerated types. For example, you can call GetNames on an enumerated type to enumerate the names of all its members. Try that in unmanaged C++!

Delegates

Newcomers to the .NET Framework often find delegates confusing. A delegate is a type-safe wrapper around a callback function. It鈥檚 rather simple to write an unmanaged C++ application that crashes when it performs a callback. It鈥檚 impossible to write a managed application that does the same, thanks to delegates.

Delegates are most commonly used to define the signatures of callback methods that are used to respond to events. For example, the FCL鈥檚 Timer class (a member of the System.Timers namespace) defines an event named Elapsed that fires whenever a preprogrammed timer interval elapses. Applications that want to respond to Elapsed events pass a Timer object a reference to the method they want called when an Elapsed event fires. The 鈥渞eference鈥?that they pass isn鈥檛 a raw memory address but rather an instance of a delegate that wraps the method鈥檚 memory address. The System.Timers namespace defines a delegate named ElapsedEventHandler for precisely that purpose.

If you could steal a look at the Timer class鈥檚 source code, you鈥檇 see something like this:

public聽delegate聽void聽ElapsedEventHandler聽(Object聽sender,聽ElapsedEventArgs聽e);

public聽class聽Timer
{
聽聽聽聽public聽event聽ElapsedEventHandler聽Elapsed;
聽聽聽聽聽聽.
聽聽聽聽聽聽.
聽聽聽聽聽聽.
}

Here鈥檚 how Timer fires an Elapsed event:

if聽(Elapsed聽!=聽null)聽//聽Make聽sure聽somebody's聽listening
聽聽聽聽Elapsed聽(this,聽new聽ElapsedEventArgs聽(...));聽//聽Fire!

And here鈥檚 how a client might use a Timer object to call a method named UpdateData every 60 seconds:

Timer聽timer聽=聽new聽Timer聽(60000);
timer.Elapsed聽+=聽new聽ElapsedEventHandler聽(UpdateData);
聽聽.
聽聽.
聽聽.
void聽UpdateData聽(Object聽sender,聽ElapsedEventArgs聽e)
{
聽聽聽聽//聽Callback聽received!
}

As you can see, UpdateData conforms to the signature specified by the delegate. To register to receive Elapsed events, the client creates a new instance of ElapsedEventHandler that wraps UpdateData (note the reference to UpdateData passed to ElapsedEventHandler鈥檚 constructor) and wires it to timer鈥檚 Elapsed event using the += operator. This paradigm is used over and over in .NET Framework applications. Events and delegates are an important feature of the type system.

In practice, it鈥檚 instructive to know more about what happens under the hood when a compiler encounters a delegate definition. Suppose the C# compiler encounters code such as this:

public聽delegate聽void聽ElapsedEventHandler聽(Object聽sender,聽ElapsedEventArgs聽e);

It responds by generating a class that derives from System.MulticastDelegate. The delegate keyword is simply an alias for something that in this case looks like this:

public聽class聽ElapsedEventHandler聽:聽MulticastDelegate
{
聽聽聽聽public聽ElapsedEventHandler聽(object聽target,聽int聽method)
聽聽聽聽{
聽聽聽聽聽聽...
聽聽聽聽}

聽聽聽聽public聽virtual聽void聽Invoke聽(object聽sender,聽ElapsedEventArgs聽e)
聽聽聽聽{
聽聽聽聽聽聽...
聽聽聽聽}
聽聽...
}

The derived class inherits several important members from MulticastDelegate, including private fields that identify the method that the delegate wraps and the object instance that implements the method (assuming the method is an instance method rather than a static method). The compiler adds an Invoke method that calls the method that the delegate wraps. C# hides the Invoke method and lets you invoke a callback method simply by using a delegate鈥檚 instance name as if it were a method name.

Boxing and Unboxing

The architects of the .NET Framework could have made every type a reference type, but they chose to support value types as well to avoid imposing undue overhead on the use of integers and other primitive data types. But there鈥檚 a downside to a type system with a split personality. To pass a value type to a method that expects a reference type, you must convert the value type to a reference type. You can鈥檛 convert a value type to a reference type per se, but you can box the value type. Boxing creates a copy of a value type on the managed heap. The opposite of boxing is unboxing, which, in C#, duplicates a reference type on the stack. Common intermediate language (CIL) has instructions for performing boxing and unboxing.

Some compilers, the C# and Visual Basic .NET compilers among them, attempt to provide a unified view of the type system by hiding boxing and unboxing under the hood. The following code wouldn鈥檛 work without boxing because it stores an int in a Hashtable object, and Hashtable objects store references exclusively:

Hashtable聽table聽=聽new聽Hashtable聽();聽//聽Create聽a聽Hashtable
table.Add聽("First",聽1);聽聽聽聽聽聽聽聽聽聽聽聽聽//聽Add聽1聽keyed聽by "First"

Here鈥檚 the CIL emitted by the C# compiler:

newobj聽聽聽聽聽instance聽void
聽聽聽聽聽聽聽聽聽聽聽[mscorlib]System.Collections.Hashtable::.ctor()
stloc.0
ldloc.0
ldstr聽聽聽聽聽 "First"
ldc.i4.1
box聽聽聽聽聽聽聽聽[mscorlib]System.Int32
callvirt聽聽聽instance聽void
聽聽聽聽聽聽聽聽聽聽聽[mscorlib]System.Collections.Hashtable::Add(object,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽object)

Notice the BOX instruction that converts the integer value 1 to a boxed value type. The compiler emitted this instruction so that you wouldn鈥檛 have to think about reference types and value types. The string used to key the Hashtable entry (鈥淔irst鈥? doesn鈥檛 have to be boxed because it鈥檚 an instance of System.String, and System.String is a reference type.

Many compilers are happy to box values without being asked to. For example, the following C# code compiles just fine:

int聽val聽=聽1;聽聽聽聽聽聽//聽Declare聽an聽instance聽of聽a聽value聽type
object聽obj聽=聽val;聽//聽Box聽it

But in C#, unboxing a reference value requires an explicit cast:

int聽val聽=聽1;
object聽obj聽=聽val;
int聽val2聽=聽obj;聽聽聽聽聽聽聽//聽This聽won't聽compile
int聽val3聽=聽(int)聽obj;聽//聽This聽will

You lose a bit of performance when you box or unbox a value, but in the vast majority of applications, such losses are more than offset by the added efficiency of storing simple data types on the stack rather than in the garbage-collected heap.

Reference Types vs. Value Types

Thanks to boxing and unboxing, the dichotomy between value types and reference types is mostly transparent to the programmer. Sometimes, however, you must know which type you鈥檙e dealing with; otherwise, subtle differences between the two can impact your application鈥檚 behavior in ways that you might not expect.

Here鈥檚 an example. The following code defines a simple reference type (class) named Point. It also declares two Point references, p1 and p2. The reference p1 is initialized with a reference to a new Point object, and p2 is initialized by setting it equal to p1. Because p1 and p2 are little more than pointers in disguise, setting one equal to the other does not make a copy of the Point object; it merely copies an address. Therefore, modifying one Point affects both:

class聽Point
{
聽聽聽聽public聽int聽x;
聽聽聽聽public聽int聽y;
}
聽聽.
聽聽.
聽聽.
Point聽p1聽=聽new聽Point聽();
p1.x聽=聽1;
p1.y聽=聽2;
Point聽p2聽=聽p1;聽//聽Copies聽the聽underlying聽pointer
p2.x聽=聽3;
p2.y聽=聽4;
Console.WriteLine聽("p1聽=聽({0},聽{1})",聽p1.x,聽p1.y);聽//聽Writes "(3,聽4)"
Console.WriteLine聽("p2聽=聽({0},聽{1})",聽p2.x,聽p2.y);聽//聽Writes "(3,聽4)"

The next code fragment is identical to the first, save for the fact that Point is now a value type (struct). But because setting one value type equal to another creates a copy of the latter, the results are quite different. Changes made to one Point no longer affect the other:

struct聽Point
{
聽聽聽聽public聽int聽x;
聽聽聽聽public聽int聽y;
}
聽聽.
聽聽.
聽聽.
Point聽p1聽=聽new聽Point聽();
p1.x聽=聽1;
p1.y聽=聽2;
Point聽p2聽=聽p1;聽//聽Makes聽a聽new聽copy聽of聽the聽object聽on聽the聽stack
p2.x聽=聽3;
p2.y聽=聽4;
Console.WriteLine聽("p1聽=聽({0},聽{1})",聽p1.x,聽p1.y);聽//聽Writes "(1,聽2)"
Console.WriteLine聽("p2聽=聽({0},聽{1})",聽p2.x,聽p2.y);聽//聽Writes "(3,聽4)"

Sometimes differences between reference types and value types are even more insidious. For example, if Point is a value type, the following code is perfectly legal:

Point聽p;
p.x聽=聽3;
p.y聽=聽4;

But if Point is a reference type, the very same instruction sequence won鈥檛 even compile. Why? Because the statement

Point聽p;

declares an instance of a value type but only a reference to a reference type. A reference is like a pointer鈥攊t鈥檚 useless until it鈥檚 initialized, as in the following:

Point聽p聽=聽new聽Point聽();

Programmers with C++ experience are especially vulnerable to this error because they see a statement that declares a reference and automatically assume that an object is being created on the stack.

The FCL contains a mixture of value types and reference types. Clearly, it鈥檚 sometimes important to know which type you鈥檙e dealing with. How do you know whether a particular FCL type is a value type or a reference type? Simple. If the documentation says it鈥檚 a class (as in 鈥淪tring Class鈥?, it鈥檚 a reference type. If the documentation says it鈥檚 a structure (for example, 鈥淒ateTime Structure鈥?, it鈥檚 a value type. Be aware of the difference, and you鈥檒l avoid frustrating hours spent in the debugger trying to figure out why code that looks perfectly good produces unpredictable results.

Nondeterministic Destruction

In traditional environments, objects are created and destroyed at precise, deterministic points in time. As an example, consider the following class written in unmanaged C++:

class聽File
{
protected:
聽聽聽聽int聽Handle;聽//聽File聽handle

public:
聽聽聽聽File聽(char*聽name)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Open聽the聽file聽and聽copy聽the聽handle聽to聽Handle
聽聽聽聽}

聽聽聽聽~File聽()
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Close聽the聽file聽handle
聽聽聽聽}
};

When you instantiate this class, the class constructor is called:

File*聽pFile聽=聽new聽File聽("Readme.txt");

And when you delete the object, its destructor is called:

delete聽pFile;

If you create the object on the stack instead of the heap, destruction is still deterministic because the class destructor is called the moment the object goes out of scope.

Destruction works differently in the .NET Framework. Remember, you create objects, but you never delete them; the garbage collector deletes them for you. But therein lies a problem. Suppose you write a File class in C#:

class聽File
{
聽聽聽聽protected聽IntPtr聽Handle聽=聽IntPtr.Zero;

聽聽聽聽public聽File聽(string聽name)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Open聽the聽file聽and聽copy聽the聽handle聽to聽Handle
聽聽聽聽}

聽聽聽聽~File聽()
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Close聽the聽file聽handle
聽聽聽聽}
}

Then you create a class instance like this:

File聽file聽=聽new聽File聽("Readme.txt");

Now ask yourself a question: when does the file handle get closed?

The short answer is that the handle gets closed when the object is destroyed. But when is the object destroyed? When the garbage collector destroys it. When does the garbage collector destroy it? Ah鈥攖here鈥檚 the key question. You don鈥檛 know. You can鈥檛 know because the garbage collector decides on its own when to run, and until the garbage collector runs, the object isn鈥檛 destroyed and its destructor isn鈥檛 called. That鈥檚 called nondeterministic destruction, or NDD. Technically, there鈥檚 no such thing as a destructor in managed code. When you write something that looks like a destructor in C#, the compiler actually overrides the Finalize method that your class inherits from System.Object. C# simplifies the syntax by letting you write something that looks like a destructor, but that arguably makes matters worse because it implies that it is a destructor, and to unknowing developers, destructors imply deterministic destruction.

Deterministic destruction doesn鈥檛 exist in framework applications unless your code does something really ugly, like this:

GC.Collect聽();

GC is a class in the System namespace that provides a programmatic interface to the garbage collector. Collect is a static method that forces a collection. Garbage collecting impedes performance, so now that you know that this method exists, forget about it. The last thing you want to do is write code that simulates deterministic destruction by calling the garbage collector periodically.

NDD is a big deal because failure to account for it can lead to all sorts of run-time errors in your applications. Suppose someone uses your File class to open a file. Later on that person uses it to open the same file again. Depending on how the file was opened the first time, it might not open again because the handle is still open if the garbage collector hasn鈥檛 run.

File handles aren鈥檛 the only problem. Take bitmaps, for instance. The FCL features a handy little class named Bitmap (it鈥檚 in the System.Drawing namespace) that encapsulates bitmapped images and understands a wide variety of image file formats. When you create a Bitmap object on a Windows machine, the Bitmap object calls down to the Windows GDI, creates a GDI bitmap, and stores the GDI bitmap handle in a field. But guess what? Until the garbage collector runs and the Bitmap object鈥檚 Finalize method is called, the GDI bitmap remains open. Large GDI bitmaps consume lots of memory, so it鈥檚 entirely conceivable that after the application has run for a while, it鈥檒l start throwing exceptions every time it tries to create a bitmap because of insufficient memory. End users won鈥檛 appreciate an image viewer utility (like the one you鈥檒l build in Chapter 4) that has to be restarted every few minutes.

So what do you do about NDD? Here are two rules for avoiding the NDD blues. The first rule is for programmers who use (rather than write) classes that encapsulate file handles and other unmanaged resources. Most such classes implement a method named Close or Dispose that releases resources that require deterministic closure. If you use classes that wrap unmanaged resources, call Close or Dispose on them the moment you鈥檙e finished using them. Assuming File implements a Close method that closes the encapsulated file handle, here鈥檚 the right way to use the File class:

File聽file聽=聽new聽File聽("Readme.txt");
聽聽.
聽聽.
聽聽.
//聽Finished聽using聽the聽file,聽so聽close聽it
file.Close聽();

The second rule, which is actually a set of rules, applies to developers who write classes that wrap unmanaged resources. Here鈥檚 a summary:

Based on these principles, here鈥檚 the right way to implement a File class:

class聽File聽:聽IDisposable
{
聽聽聽聽protected聽IntPtr聽Handle聽=聽IntPtr.Zero;

聽聽聽聽public聽File聽(string聽name)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Open聽the聽file聽and聽copy聽the聽handle聽to聽Handle.
聽聽聽聽}

聽聽聽聽~File聽()
聽聽聽聽{
聽聽聽聽聽聽聽聽Dispose聽(false);
聽聽聽聽}

聽聽聽聽public聽void聽Dispose聽()
聽聽聽聽{
聽聽聽聽聽聽聽聽GC.SuppressFinalize聽(this);
聽聽聽聽聽聽聽聽Dispose聽(true);
聽聽聽聽}

聽聽聽聽protected聽virtual聽void聽Dispose聽(bool聽disposing)
聽聽聽聽{
聽聽聽聽聽聽聽聽//聽TODO:聽Close聽the聽file聽handle.
聽聽聽聽聽聽聽聽if聽(disposing)聽{
聽聽聽聽聽聽聽聽聽聽聽聽//聽TODO:聽If聽the聽class聽has聽members聽that聽wrap
聽聽聽聽聽聽聽聽聽聽聽聽//聽unmanaged聽resources,聽call聽Close聽or聽Dispose聽on
聽聽聽聽聽聽聽聽聽聽聽聽//聽them聽here.
聽聽聽聽聽聽聽聽}
聽聽聽聽}

聽聽聽聽public聽void聽Close聽()
聽聽聽聽{
聽聽聽聽聽聽聽聽Dispose聽();
聽聽聽聽}
}

Note that the 鈥渄estructor鈥濃攁ctually, the Finalize method鈥攏ow calls the protected Dispose with a false parameter, and that the public Dispose calls the protected Dispose and passes in true. The call to GC.SuppressFinalize is both a performance optimization and a measure to prevent the handle from being closed twice. Because the object has already closed the file handle, there鈥檚 no need for the garbage collector to call its Finalize method. It鈥檚 still important to override the Finalize method to ensure proper disposal if Close or Dispose isn鈥檛 called.