Previous Page
Next Page

The Generics Solution

Generics was added to C# 2.0 to remove the need for casting, improve type safety, reduce the amount of boxing required, and to make it easier to create generalized classes and methods. Generic classes and methods accept type parameters, which specify the type of objects that they operate on. Version 2.0 of the .NET Framework Class Library includes generic versions of many of the collection classes and interfaces in the System.Collections.Generic namespace. The following code fragment shows how to use the generic Queue class found in this namespace to create a queue of Circle objects:

using System.Collections.Generic;
Queue<Circle> myQueue = new Queue<Circle>();
Circle myCircle = new Circle();
myCircle = myQueue.Dequeue();

There are two new things to note about the code in the above sample:

The type parameter specifies the type of objects accepted by the queue. All references to methods in this queue will automatically expect to use this type rather than object, rendering the cast to the Circle type when invoking the Dequeue method unnecessary. The compiler will check to ensure that types are not accidentally mixed, generating an error at compile time rather than runtime if you try to dequeue an item from circleQueue into a Clock object, for example.

If you examine the description of the generic Queue class in the Visual Studio 2005 Documentation, you will notice that it is defined as:

public class Queue<T> : …

The T identifies the type parameter and acts as a placeholder for a real type at compile time. When you write code to instantiate a generic Queue, you provide the type that should be substituted for T, as shown in the previous example which specifies Circle. Furthermore, if you then look at the methods of the Queue<T> class you will observe that some of them, such as Enqueue and Dequeue, specify T as a parameter type or return value:

public void Enqueue( T item );
public T Dequeue();

The type parameter, T, will be replaced with the type you specified when you declared the queue. What is more, the compiler now has enough information to perform strict type checking when you build the application and can trap any type mismatch errors early.

You should also be aware that this substitution of T for a specified type is not simply a textual replacement mechanism. Instead, the compiler performs a complete semantic substitution allowing you to specify any valid type for T. Here are more examples:

struct Person
Queue<int> intQueue = new Queue<int>();
Queue<Person> personQueue = new Queue<Person>();
Queue<Queue<int>> queueQueue = new Queue<Queue<int>>();

The first two examples create queues of value types, while the third creates a queue of queues (of ints). If we take the intQueue variable as an example, the compiler will also generate the following versions of the Enqueue and Dequeue methods:

public void Enqueue( int item );
public int Dequeue();

Contrast these definitions with those of the non-generic Queue class shown in the previous section. In the methods derived from the generic class, the item parameter to Enqueue is passed as a value type that does not require boxing. Similarly, the value returned by Dequeue is also a value type that does not need to be unboxed.

It is also possible for a generic class to have multiple type parameters. For example, the generic System.Collecions.Generic.Dictionary class expects two type parameters: one type for keys, and another for the values. The following definition shows how to specify multiple type parameters:

public class Dictionary<T, U>

A dictionary provides a collection of key/value pairs. You store values (type U) with an associated key (type T), and then retrieve them by specifying the key to look up. The Dictionary class provides an indexer that allows you to access items by using array notation. It is defined like this:

public virtual U this[ T key ] { get; set; }

Notice that the indexer accesses values of type U by using a key of type T. To create and use a dictionary, called directory, containing Person values identified by string keys, you could use the following code:

struct Person
Dictionary<string, Person> directory = new Dictionary<string, Person>();
Person john = new Person();
directory["John"] = john;
john = directory["John"];

As with the generic Queue class, the compiler will detect attempts to store values other than Person structures, as well as ensuring that the key is always a string value. For more information about the Dictionary class, you should read the Visual Studio 2005 documentation.

You can also define generic structs and interfaces by using the same syntax as generic classes.
Generics vs. Generalized Classes

It is important to be aware that a generic class that uses type parameters is different from a generalized class designed to take parameters that can be cast to different types. For example, the System.Collections.Queue class is a generalized class. There is a single implementation of this class, and its methods take object parameters and return object types. You can use this class with ints, strings, and many other types, but these are all instances of the same class.

Contrast this with the System.Collections.Generic.Queue<T> class. Each time you use this class with a type parameter (such as Queue<int> or Queue<string>) you actually cause the compiler to generate an entirely new class that happens to have functionality defined by the generic class. You can think of a generic class as one that defines a template that is then used by the compiler to generate new type-specific classes on demand. The type-specific versions of a generic class (Queue<int>, Queue<string>, and so on) are referred to as constructed types.

Generics and Constraints

Sometimes there will be occasions when you want to ensure that the type parameter used by a generic class identifies a type that provides certain methods. For example, if you are defining a PrintableCollection class, you might want to ensure that all objects stored in the class have a Print method. You can specify this condition by using a constraint.

Using a constraint enables you to limit the type parameters of a generic class to those that implement a particular set of interfaces, and therefore provide the methods defined by those interfaces. For example, if the IPrintable interface contained the Print method, you could define the PrintableCollection class like this:

public class PrintableCollection<T> where T : IPrintable

When the class is compiled, the compiler will check to ensure that the type used for T actually implements the IPrintable interface and will stop with a compilation error if it doesn't.

Previous Page
Next Page