In order to understand Generics, it is worth looking in detail at the problems they are designed to solve, specifically when using the object type.
You can use the object type as a reference to any type of value or variable. All reference types automatically inherit (either directly or indirectly) from the System.Object class in the .NET Framework. You can use this information to create highly generalized classes and methods. For example, many of the classes in the System.Collections namespace exploit this fact to allow you to create collections of any type. You will also notice in the System.Collections.Queue class that you can create queues containing almost anything (you have already been introduced to the collection classes in Chapter 10, “Using Arrays and Collections”). The following fragment shows how to create and manipulate a queue of Circle objects:
using System.Collections; ... Queue myQueue = new Queue(); Circle myCircle = new Circle(); myQueue.Enqueue(myCircle); ... myCircle = (Circle)myQueue.Dequeue();
The Enqueue method adds an object to the head of a queue, and the Dequeue method removes the object at the other end of the queue. These methods are defined like this:
public void Enqueue( object item ); public object Dequeue();
Because the Enqueue and Dequeue methods manipulate objects, you can operate on queues of Circles, PhoneBooks, Clocks, or any of the other classes you have seen in earlier exercises in this book. However, it is important to notice that you have to cast the value returned by the Dequeue method to the appropriate type because the compiler will not perform the conversion from the object type automatically. If you don't cast the returned value, you will get the compiler error “Cannot implicitly convert type 'object' to 'Circle'” as shown in the following code fragment:
Circle myCircle = new Circle(); myQueue.Enqueue(myCircle); ... myCircle = (Circle)myQueue.Dequeue(); // Cast is mandatory
This need to perform an explicit cast denigrates much of the flexibility afforded by the object type. It is very easy to write code such as this:
Queue myQueue = new Queue(); Circle myCircle = new Circle(); myQueue.Enqueue(myCircle); ... Clock myClock = (Clock)myQueue.Dequeue();
Although this code will compile, it is not valid and throws a System.InvalidCastException at runtime. The error is caused by trying to store a reference to a Circle in a Clock variable, and the two types are not compatible. This error is not spotted until runtime because the compiler does not have enough information. It can only determine the real type of the object being dequeued at runtime.
Another disadvantage of using the object approach to create generalized classes and methods is that it can use additional memory and processor time if the runtime needs to convert an object into a value type and back again. Consider the following piece of code that manipulates a queue of ints:
Queue myQueue = new Queue(); int myInt = 99; myQueue.Enqueue(myInt); // box the int to an object ... myInt = (int)myQueue.Dequeue(); // unbox the object to an int
The Queue data type expects the items it holds to be reference types. Enqueueing a value type, such as an int, requires that it is boxed to convert it into a reference type. Similarly, dequeueing into an int requires that the item is unboxed to convert it back to a value type. See the sections “Boxing” and “Unboxing” in Chapter 8 for more details. Although boxing and unboxing happen transparently, they add a performance overhead as they involve dynamic memory allocations. While this overhead is small for each item, it adds up when a program creates queues of large numbers of value types.