Using Generics in C# 2.0
- C# without Generics
- Introducing Generic Types
- Constraints
- Generic Methods
- Generic Internals
- Summary
As your projects become more sophisticated, you will need a better way to reuse and customize existing software. To facilitate code reuse, especially the reuse of algorithms, C# includes a feature called generics. Just as methods are powerful because they can take parameters, classes that take type parameters have significantly more functionality as well, and this is what generics enable. Like their predecessor, templates, generics enable the definition of algorithms and pattern implementations once, rather than separately for each type. However, C# implements a type-safe version of templates that differs slightly in syntax and greatly in implementation from its predecessors in C++ and Java. Note that generics were added to the runtime and C# with version 2.0.
C# without Generics
I will begin the discussion of generics by examining a class that does not use generics. The class is System.Collections.Stack, and its purpose is to represent a collection of objects such that the last item to be added to the collection is the first item retrieved from the collection (called last in, first out, or LIFO). Push() and Pop(), the two main methods of the Stack class, add items to the stack and remove them from the stack, respectively. The declarations for the Pop() and Push() methods on the stack class appear in Listing 11.1.
Example 11.1. The Stack Definition Using a Data Type Object
public class Stack { public virtual object Pop(); public virtual void Push(object obj); // ... }
Programs frequently use stack type collections to facilitate multiple undo operations. For example, Listing 11.2 uses the stack class for undo operations within a program which simulates the Etch A Sketch game.
Example 11.2. Supporting Undo in a Program Similar to the Etch A Sketch Game
using System; using System.Collections; class Program { // ... public void Sketch() { Stack path = new Stack(); Cell currentPosition; ConsoleKeyInfo key; // New with C# 2.0 do { // Etch in the direction indicated by the // arrow keys that the user enters. key = Move(); switch (key.Key) { case ConsoleKey.Z: // Undo the previous Move. if (path.Count >= 1) { currentPosition = (Cell)path.Pop(); Console.SetCursorPosition( currentPosition.X, currentPosition.Y); Undo(); } break ; case ConsoleKey.DownArrow: case ConsoleKey.UpArrow: case ConsoleKey.LeftArrow: case ConsoleKey.RightArrow: // SaveState() currentPosition = new Cell( Console.CursorLeft, Console.CursorTop); path.Push(currentPosition); break ; default: Console.Beep(); // New with C#2.0 break ; } } while (key.Key != ConsoleKey.X); // Use X to quit. } } public struct Cell { readonly public int X; readonly public int Y; public Cell(int x, int y) { X = x; Y = y; } }
The results of Listing 11.2 appear in Output 11.1.
Using the variable path, which is declared as a System.Collections.Stack, you save the previous move by passing a custom type, Cell, into the Stack.Push() method using path.Push(currentPosition). If the user enters a Z (or Ctrl+Z), then you undo the previous move by retrieving it from the stack using a Pop() method, setting the cursor position to be the previous position, and calling Undo(). (Note that this code uses some CLR 2.0-specific console functions as well.)
Example 11.1.
Although the code is functional, there is a fundamental drawback in the System.Collections.Stack class. As shown in Listing 11.1, the Stack class collects variables of type object. Because every object in the CLR derives from object, Stack provides no validation that the elements you place into it are homogenous or are of the intended type. For example, instead of passing currentPosition, you can pass a string in which X and Y are concatenated with a decimal point between them. However, the compiler must allow the inconsistent data types because in some scenarios, it is desirable.
Furthermore, when retrieving the data from the stack using the Pop() method, you must cast the return value to a Cell. But if the value returned from the Pop() method is not a Cell type object, an exception is thrown. You can test the data type, but splattering such checks builds complexity. The fundamental problem with creating classes that can work with multiple data types without generics is that they must use a common base type, generally object data.
Using value types, such as a struct or integer, with classes that use object exacerbates the problem. If you pass a value type to the Stack.Push() method, for example, the runtime automatically boxes it. Similarly, when you retrieve a value type, you need to explicitly unbox the data and cast the object reference you obtain from the Pop() method into a value type. While the widening operation (cast to a base class) for a reference type has a negligible performance impact, the box operation for a value type introduces nontrivial overhead.
To change the Stack class to enforce storage on a particular data type using the preceding C# programming constructs, you must create a specialized stack class, as in Listing 11.3.
Example 11.3. Defining a Specialized Stack Class
public class CellStack { public virtual Cell Pop(); public virtual void Push(Cell cell); // ... }
Because CellStack can store only objects of type Cell, this solution requires a custom implementation of the stack methods, which is less than ideal.