InformIT

Using Generics in C# 2.0

Date: Oct 6, 2006

Sample Chapter is provided courtesy of Addison-Wesley Professional.

Return to the article

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. Mark Michaelis discusses generics in C# in this sample chapter.

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.

michaelis_fig11_02.jpg

Click to view larger image

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.

Introducing Generic Types

Generics provide a facility for creating data structures that are specialized to handle specific types when declaring a variable. Programmers define these parameterized types so that each variable of a particular generic type has the same internal algorithm but the types of data and method signatures can vary based on programmer preference.

To minimize the learning curve for developers, C# designers chose syntax that matched the similar templates concept of C++. In C#, therefore, the syntax for generic classes and structures uses the same angle bracket notation to identify the data types on which the generic declaration specializes.

Using a Generic Class

Listing 11.6 shows how you can specify the actual type used by the generic class. You instruct the path variable to use a Cell type by specifying Cell within angle bracket notation in both the instantiation and the declaration expressions. In other words, when declaring a variable (path in this case) using a generic data type, C# requires the developer to identify the actual type. An example showing the new Stack class appears in Listing 11.6.

Example 11.6. Implementing Undo with a Generic Stack Class


   using System;
using System.Collections.Generic;

class Program
{
  // ...
 
  
   public 
   void Sketch()
{
      Stack<Cell> path;         // Generic variable declaration        
   

         path = new Stack<Cell>(); // Generic object instantiation       
   
      Cell currentPosition;
      ConsoleKeyInfo key;        // New with C# 2.0

      
   do
      {
          // Etch in the direction indicated by the
          
   // arrow keys entered by the user.
          key = Move();
      switch (key.Key)
      {
          case ConsoleKey.Z:
              // Undo the previous Move.
         
   
              if (path.Count >= 1)
              {
                  
      // No cast required.                            
   

                     currentPosition = 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);
              
      // Only type Cell allowed in call to Push().          
   

                 path.Push(currentPosition);                           
             
   break;

          
   default:
              Console.Beep();  // New with C#2.0
              
   break;
      }

  } while (key.Key != ConsoleKey.X); // Use X to quit.

 }
}

The results of Listing 11.6 appear in Output 11.2.

In the path declaration shown in Listing 11.6, you declare and create a new instance of a System.Collections.Generic.Stack<T> class and specify in angle brackets that the data type used for the path variable is Cell. As a result, every object added to and retrieved from path is of type Cell. In other words, you no longer need to cast the return of path.Pop() or ensure that only Cell type objects are added to path in the Push() method. Before examining the generic advantages, the next section introduces the syntax for generic class definitions.

Example 11.2.

michaelis_fig11_07.jpg

Click to view larger image

Defining a Simple Generic Class

Generics allow you to author algorithms and patterns, and reuse the code for different data types. Listing 11.7 creates a generic Stack<T> class similar to the System.Collections.Generic.Stack<T> class used in the code in Listing 11.6. You specify a type parameter identifier or type parameter (in this case, T) within angle brackets after the class declaration. Instances of the generic Stack<T> then collect the type corresponding to the variable declaration without converting the collected item to type object. The type parameter T is a placeholder until variable declaration and instantiation, when the compiler requires the code to specify the type parameter. In Listing 11.7, you can see that the type parameter will be used for the internal Items array, the type for the parameter to the Push() method, and the return type for the Pop() method.

Example 11.7. Declaring a Generic Class, Stack<T>


   public class Stack<T>
{
    private T[] _Items;

    public void Push(T data)
    {
        ...
    }

    public T Pop()
    {
         ...
    }
}

Benefits of Generics

There are several advantages to using a generic class (such as the System.Collections.Generic.Stack<T> class used earlier instead of the original System.Collections.Stack type).

  1. Generics facilitate a strongly typed programming model, preventing data types other than those explicitly intended by the members within the parameterized class. In Listing 11.7, the parameterized stack class restricts you to the Cell data type for all instances of Stack<Cell>. (The statement path.Push("garbage") produces a compile-time error indicating that there is no overloaded method for System.Collections.Generic.Stack<T>.Push(T) that can work with the string garbage, because it cannot be converted to a Cell.)
  2. Compile-time type checking reduces the likelihood of InvalidCastException type errors at runtime.
  3. Using value types with generic class members no longer causes a cast to object; they no longer require a boxing operation. (For example, path.Pop() and path.Push() do not require an item to be boxed when added or unboxed when removed.)
  4. Generics in C# reduce code bloat. Generic types retain the benefits of specific class versions, without the overhead. (For example, it is no longer necessary to define a class such as CellStack.)
  5. Performance increases because casting from an object is no longer required, thus eliminating a type check operation. Also, performance increases because boxing is no longer necessary for value types.
  6. Generics reduce memory consumption because the avoidance of boxing no longer consumes memory on the heap.
  7. Code becomes more readable because of fewer casting checks and because of the need for fewer type-specific implementations.
  8. Editors that assist coding via some type of IntelliSense work directly with return parameters from generic classes. There is no need to cast the return data for IntelliSense to work.

At their core, generics offer the ability to code pattern implementations and then reuse those implementations wherever the patterns appear. Patterns describe problems that occur repeatedly within code, and templates provide a single implementation for these repeating patterns.

Type Parameter Naming Guidelines

Just as when you name a method parameter, you should be as descriptive as possible when naming a type parameter. Furthermore, to distinguish it as being a type parameter, the name should include a "T" prefix. For example, in defining a class such as EntityCollection<TEntity> you use the type parameter name "TEntity."

The only time you would not use a descriptive type parameter name is when the description would not add any value. For example, using "T" in the Stack<T> class is appropriate since the indication that "T" is a type parameter is sufficiently descriptive; the stack works for any type.

In the next section, you will learn about constraints. It is a good practice to use constraint descriptive type names. For example, if a type parameter must implement IComponent, consider a type name of "TComponent."

Generic Interfaces and Structs

C# 2.0 supports the use of generics in all parts of the C# language, including interfaces and structs. The syntax is identical to that used by classes. To define an interface with a type parameter, place the type parameter in angle brackets, as shown in the example of IPair<T> in Listing 11.8.

Example 11.8. Declaring a Generic Interface


   interface IPair<T>
{
    T First
    {
        get;
        set;
    }

    T Second
    {
        get;
        set;
    }
}

This interface represents pairs of like objects, such as the coordinates of a point, a person's genetic parents, or nodes of a binary tree. The type contained in the pair is the same for both items.

To implement the interface, you use the same syntax as you would for a nongeneric class. However, implementing a generic interface without identifying the type parameter forces the class to be a generic class, as shown in Listing 11.9. In addition, this example uses a struct rather than a class, indicating that C# supports custom generic value types.

Example 11.9. Implementing a Generic Interface


   public 
   struct Pair<T>: IPair<T>
{
    public T First
    {
        get
        {
            return _First;
        }
        set
        {
            _First = value;
        }
    }
    private T _First;
 
    public T Second
    {
        get
        {
            return _Second;
        }
        set
        {
            _Second = value;
        }
    }
    private T _Second;
}

Support for generic interfaces is especially important for collection classes, where generics are most prevalent. Without generics, developers relied on a series of interfaces within the System.Collections namespace. Like their implementing classes, these interfaces worked only with type object, and as a result, the interface forced all access to and from these collection classes to require a cast. By using generic interfaces, you can avoid cast operations, because a stronger compile-time binding can be achieved with parameterized interfaces.

Defining a Constructor and a Finalizer

Perhaps surprisingly, the constructor and destructor on a generic do not require type parameters in order to match the class declaration (i.e., not Pair<T>(){...}). In the pair example in Listing 11.11, the constructor is declared using public Pair(T first, T second).

Example 11.11. Declaring a Generic Type's Constructor


   public 
   struct Pair<T>: IPair<T>
{
  
      public Pair(T first, T second)                    

    {                                                   

        _First = first;                                

        _Second = second;                               

    }                                                   

  
   public T First
  {
      get{ return _First; }
      set{ _First = value; }
  }
  private T _First;

  public T Second
  {
      get{ return _Second; }
      set{ _Second = value; }
  }
  private T _Second;
}

Specifying a Default Value

Listing 11.1 included a constructor that takes the initial values for both First and Second, and assigns them to _First and _Second. Since Pair<T> is a struct, any constructor you provide must initialize all fields. This presents a problem, however. Consider a constructor for Pair<T> that initializes only half of the pair at instantiation time.

Defining such a constructor, as shown in Listing 11.12, causes a compile error because the field _Second goes uninitialized at the end of the constructor. Providing initialization for _Second presents a problem since you don't know the data type of T. If it is a reference type, then null would work, but this would not suffice if T were a value type (unless it was nullable).

Example 11.12. Not Initializing All Fields, Causing a Compile Error


   public 
   struct Pair<T>: IPair<T>
{
  // ERROR:  Field 'Pair<T>._second' must be fully assigned
  
   //         before control leaves the constructor
  
   // 
   public Pair(T first)
  // {
  //     _First = first;
  // }
 
  // ...
}

To deal with this scenario, C# 2.0 allows a dynamic way to code the default value of any data type using the default operator, first discussed in Chapter 8. In Chapter 8, I showed how the default value of int could be specified with default(int) while the default value of a string uses default(string) (which returns null, as it would for all reference types). In the case of T, which _Second requires, you use default(T) (see Listing 11.13).

Example 11.13. Initializing a Field with the default Operator


   public 
   struct Pair<T>: IPair<T>
{
  public Pair(T first)
  { 
      _First = first;
      _Second = default(T);                                
  }

     // ...
}

The default operator is allowable outside of the context of generics; any statement can use it.

Multiple Type Parameters

Generic types may employ any number of type parameters. The initial Pair<T> example contains only one type parameter. To enable support for storing a dichotomous pair of objects, such as a name/value pair, you need to extend Pair<T> to support two type parameters, as shown in Listing 11.14.

Example 11.14. Declaring a Generic with Multiple Type Parameters


   interface IPair<TFirst, TSecond>
{
    TFirst First
    { get; set;     }

    TSecond Second
    { get; set;     }
}

public 
   struct Pair<TFirst, TSecond>: IPair<TFirst, TSecond>
{
    public Pair(TFirst first, TSecond second)
    { 
        _First = first;
        _Second = second;
    }

    public TFirst First
    {
        get{ return _First;     }
        set{ _First = value;  }
    }
    private TFirst _First;

    public TSecond Second
    {
        get{ return _Second; }
        set{ _Second = value; }
    }
    private TSecond _Second;
}

When you use the Pair<TFirst, TSecond> class, you supply multiple type parameters within the angle brackets of the declaration and instantiation statements, and then you supply matching types to the parameters of the methods when you call them, as shown in Listing 11.15.

Example 11.15. Using a Type with Multiple Type Parameters

Pair<int, string> historicalEvent =
    new Pair<int, string>(1914,
        "Shackleton leaves for South Pole on ship Endurance");
Console.WriteLine("{0}: {1}",
    historicalEvent.First, historicalEvent.Second);

The number of type parameters, the arity, uniquely distinguishes the class. Therefore, it is possible to define both Pair<T> and Pair<TFirst, TSecond> within the same namespace because of the arity variation.

Nested Generic Types

Nested types will automatically inherit the type parameters of the containing type. If the containing type includes a type parameter T, for example, then the type T will be available on the nested type as well. If the nested type includes its own type parameter named T, then this will hide the type parameter within the containing type and any reference to T in the nested type will refer to the nested T type parameter. Fortunately, reuse of the same type parameter name within the nested type will cause a compiler warning to prevent accidental overlap (see Listing 11.16).

Example 11.16. Nested Generic Types


   class Container<T, U>
{
  // Nested classes inherit type parameters.
  
   // Reusing a type parameter name will cause
  
   // a warning.
  
   class Nested<U>
  {
     
      void Method(T param0, U param1)                          
     {
     }
  }
}       

The behavior of making the container's type parameter available in the nested type is consistent with nested type behavior in the sense that private members of the containing type are also accessible from the nested type. The rule is simply that a type is available anywhere within the curly braces within which it appears.

Type Compatibility between Generic Classes with Type-Compatible Type Parameters

If you declare two variables with different type parameters using the same generic class, the variables are not type compatible; they are not covariant. The type parameter differentiates two variables of the same generic class but with different type parameters. For example, instances of a generic class, Stack<Contact> and Stack<PdaItem>, are not type compatible even when the type parameters are compatible. In other words, there is no built-in support for casting Stack<Contact> to Stack<PdaItem>, even though Contact derives from PdaItem (see Listing 11.17).

Example 11.17. Conversion between Generics with Different Type Parameters


   using System.Collections.Generic;
...
// Error: Cannot convert type ...
Stack<PdaItem> exceptions = new Stack<Contact>();

To allow this you would have to subtly cast each instance of the type parameter, possibly an entire array or collection, which would hide a potentially significant performance cost.

Constraints

Generics support the ability to define constraints on type parameters. These constraints enforce the types to conform to various rules. Take, for example, the BinaryTree<T> class shown in Listing 11.18.

Example 11.18. Declaring a BinaryTree<T> Class with No Constraints


   public 
   class BinaryTree<T>
{
    public BinaryTree ( T item)
    {
        Item = item;

    }

    public T Item
    {
        get{ return _Item; }
        set{ _Item = value; }
    }
    private T _Item;

    public Pair<BinaryTree<T>> SubItems
    {
        get{ return _SubItems; }
        set{ _SubItems = value; }
     }
     private Pair<BinaryTree<T>> _SubItems;
 }

(An interesting side note is that BinaryTree<T> uses Pair<T> internally, which is possible because Pair<T> is simply another type.)

Suppose you want the tree to sort the values within the Pair<T> value as it is assigned to the SubItems property. In order to achieve the sorting, the SubItems get accessor uses the CompareTo() method of the supplied key, as shown in Listing 11.19.

Example 11.19. Needing the Type Parameter to Support an Interface


   public 
   class BinaryTree<T>
{
    ...
    public Pair<BinaryTree<T>> SubItems
    {
        get{ return _SubItems; }
        set
        {
            IComparable first;                                            

               
      // ERROR: Cannot implicitly convert type...                   
   

               first = value.First.Item    // Explicit cast required                
   
                                                                        

               
      if (first.CompareTo(value.Second.Item) < 0)                   

               {                                                             

                   
      // first is less than second.                             
   

                  ...                                                        

               }                                                             

               
      else                                                          
   

               {                                                             

                  
      // first and second are the same or                        
   

                  // second is less than first.                              

                  ...                                                        

               }                                                             

               _SubItems = value;                                            

              
   } 
         }
         private Pair<BinaryTree<T>> _SubItems;
  }

At compile time, the type parameter T is generic. Written as is, the compiler assumes that the only members available on T are those inherited from the base type object, since every type has object as an ancestor. (Only methods such as ToString(), therefore, are available to the key instance of the type parameter T.) As a result, the compiler displays a compilation error because the CompareTo() method is not defined on type object.

You can cast the T parameter to the IComparable interface in order to access the CompareTo() method, as shown in Listing 11.20.

Example 11.20. Needing the Type Parameter to Support an Interface or Exception Thrown


   public 
   class BinaryTree<T>
{
    ...
    public Pair<BinaryTree<T>> SubItems
    {
        get{ return _SubItems; }
        set
        {
            IComparable first;
            first = (IComparable)value.First.Item;                  
                                                              

               
      if (first.CompareTo(value.Second.Item) < 0)             
            {
                // first is less than second.
                 ... 
            }
            else
            {
                // second is less than or equal to first.
                ... 
            }
            _SubItems = value; 
         }
     }
     private Pair<BinaryTree<T>> _SubItems;
 }

Unfortunately, however, if you now declare a BinaryTree class variable and supply a type parameter that does not implement the IComparable interface, you encounter an execution-time error—specifically, an InvalidCastException. This defeats an advantage of generics.

To avoid this exception and instead provide a compile-time error, C# enables you to supply an optional list of constraints for each type parameter declared in the generic class. A constraint declares the type parameter characteristics that the generic requires. You declare a constraint using the where keyword, followed by a "parameter-requirements" pair, where the parameter must be one of those defined in the generic type and the requirements are to restrict the class or interface from which the type "derives," the presence of a default constructor, or a reference/value type restriction.

Interface Constraints

In order to satisfy the sort requirement, you need to use the CompareTo() method in the BinaryTree class. To do this most effectively, you impose a constraint on the T type parameter. You need the T type parameter to implement the IComparable interface. The syntax for this appears in Listing 11.21.

Example 11.21. Declaring an Interface Constraint


   public 
   class BinaryTree<T>
    
      where T: System.IComparable                           
{
    ...
    public Pair<BinaryTree<T>> SubItems
    {
        get{ return _SubItems; }
        set
        {
            IComparable first;
       
      // Notice that the cast can now be eliminated.     
   

          first = value.First.Item;                          
                                                          

         
      if (first.CompareTo(value.Second.Item) < 0)         
            {
                 // first is less than second
                 
   ...
            }
            else
            {
                // second is less than or equal to first.
                
   ...
            }      
            _SubItems = value;
        }
    }
    private Pair<BinaryTree<T>> _SubItems;
}

Given the interface constraint addition in Listing 11.21, the compiler ensures that each time you use the BinaryTree class you specify a type parameter that implements the IComparable interface. Furthermore, you no longer need to explicitly cast the variable to an IComparable interface before calling the CompareTo() method. Casting is not even required to access members that use explicit interface implementation, which in other contexts would hide the member without a cast. To resolve what member to call, the compiler first checks class members directly, and then looks at the explicit interface members. If no constraint resolves the argument, only members of object are allowable.

If you tried to create a BinaryTree<T> variable using System.Text.StringBuilder as the type parameter, you would receive a compiler error because StringBuilder does not implement IComparable. The error is similar to the one shown in Output 11.3.

Example 11.3.

error CS0309: The type 'System.Text.StringBuilder>' must be convertible
to
'System.IComparable' in order to use it
as parameter 'T' in the generic type or method 'BinaryTree<T>'

To specify an interface for the constraint you declare an interface constraint. This constraint even circumvents the need to cast in order to call an explicit interface member implementation.

Base Class Constraints

Sometimes you might want to limit the constructed type to a particular class derivation. You do this using a base class constraint, as shown in Listing 11.22.

Example 11.22. Declaring a Base Class Constraint


   public 
   class EntityDictionary<TKey, TValue>
   : System.Collections.Generic.Dictionary<TKey, TValue>
   where TValue : EntityBase
{
 ...
}

In contrast to System.Collections.Generic.Dictionary<TKey, TValue> on its own, EntityDictionary<TKey, TValue> requires that all TValue types derive from the EntityBase class. By requiring the derivation, it is possible to always perform a cast operation within the generic implementation, because the constraint will ensure that all type parameters derive from the base and, therefore, that all TValue type parameters used with EntityDictionary can be implicitly converted to the base.

The syntax for the base class constraint is the same as that for the interface constraint, except that base class constraints must appear first when multiple constraints are specified. However, unlike interface constraints, multiple base class constraints are not allowed since it is not possible to derive from multiple classes. Similarly, base class constraints cannot be specified for sealed classes or specific structs. For example, C# does not allow a constraint for a type parameter to be derived from string or System.Nullable<T>.

struct/class Constraints

Another valuable generic constraint is the ability to restrict type parameters to a value type or a reference type. The compiler does not allow specifying System.ValueType as the base class in a constraint. Instead, C# provides special syntax that works for reference types as well. Instead of specifying a class from which T must derive, you simply use the keyword struct or class, as shown in Listing 11.23.

Example 11.23. Specifying the Type Parameter as a Value Type


   public 
   struct Nullable<T> :
     IFormattable, IComparable,
     IComparable<Nullable<T>>, INullable
      
      where T : struct                              
   
{
     // ...
}

Because a base class constraint requires a particular base class, using struct or class with a base class constraint would be pointless, and in fact could allow for conflicting constraints. Therefore, you cannot use struct and class constraints with a base class constraint.

There is one special characteristic for the struct constraint. It limits possible type parameters as being only value types while at the same time preventing type parameters that are System.Nullable<T> type parameters. Why? Without this last restriction, it would be possible to define the nonsense type Nullable<Nullable<T>>, which is nonsense because Nullable<T> on its own allows a value type variable that supports nulls, so a nullable-nullable type becomes meaningless. Since the nullable operator (?) is a C# shortcut for declaring a nullable value type, the Nullable<T> restriction provided by the struct constraint also prevents code such as the following:

 int?? number  // Equivalent to Nullable<Nullable<int> if allowed

Multiple Constraints

For any given type parameter, you may specify any number of interfaces as constraints, but no more than one class, just as a class may implement any number of interfaces but inherit from only one other class. Each new constraint is declared in a comma-delimited list following the generic type and a colon. If there is more than one type parameter, each must be preceded by the where keyword. In Listing 11.24, the EntityDictionary class contains two type parameters: TKey and TValue. The TKey type parameter has two interface constraints, and the TValue type parameter has one base class constraint.

Example 11.24. Specifying Multiple Constraints


   public 
   class EntityDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>
    where TKey : IComparable, IFormattable
    where TValue : EntityBase
{
   ...
}

In this case, there are multiple constraints on TKey itself and an additional constraint on TValue. When specifying multiple constraints on one type parameter, an AND relationship is assumed. TKey must implement IComparable and IFormattable, for example. Notice there is no comma between each where clause.

Constructor Constraints

In some cases, it is desirable to create an instance of a type parameter inside the generic class. In Listing 11.25, the New() method for the EntityDictionary<TKey, TValue> class must create an instance of the type parameter TValue.

Example 11.25. Requiring a Default Constructor Constraint


   public 
   class EntityBase<TKey>
{
    public TKey Key
    {
        get{ return _Key; }
        set{ _Key = value; }
    }
    private TKey _Key;
}

public 
   class EntityDictionary<TKey, TValue> :
    Dictionary<TKey, TValue>
    where TKey: IComparable, IFormattable
      
      where TValue : EntityBase<TKey>, new()           
   
{
    // ...

    
   public TValue New(TKey key)
    {
       TValue newEntity = new TValue();                
        newEntity.Key = key;
        Add(newEntity.Key, newEntity);
        return newEntity;
    }

    // ...
}

Because not all objects are guaranteed to have public default constructors, the compiler does not allow you to call the default constructor on the type parameter. To override this compiler restriction, you add the text new() after all other constraints are specified. This text is a constructor constraint , and it forces the type parameter decorated with the constructor constraint to have a default constructor. Only the default constructor constraint is available. You cannot specify a constraint for a constructor with parameters.

Constraint Inheritance

Constraints are inherited by a derived class, but they must be specified explicitly on the derived class. Consider Listing 11.26.

Example 11.26. Inherited Constraints Specified Explicitly


   class EntityBase<T> where T : IComparable
{
}

// ERROR:

   // The type 'T' must be convertible to 'System.IComparable'

   // in order to use it as parameter 'T' in the generic type or

   // method.

   // 
   class Entity<T> : EntityBase<T>
// {
// }

Because EntityBase requires that T implement IComparable, the Entity class needs to explicitly include the same constraint. Failure to do so will result in a compile error. This increases a programmer's awareness of the constraint in the derived class, avoiding confusion when using the derived class and discovering the constraint, but not understanding where it comes from.

Generic Methods

You already learned that it is relatively simple to add a generic method to a class when the class is a generic. You did this in the generic class examples so far, and it also works for static methods. Furthermore, you can use generic classes within a generic class, as you did in earlier BinaryTree listings using the following line of code:

public Pair< BinaryTree<T> > SubItems;

Generic methods are methods that use generics even when the containing class is not a generic class or the method contains type parameters not included in the generic class type parameter list. To define generic methods, you add the type parameter syntax immediately following the method name, as shown in the MathEx.Max<T> and MathEx.Min<T> examples in Listing 11.33.

Example 11.33. Defining Generic Methods


   public static class MathEx
{
  public 
   static T Max<T>(T first, params T[] values)
      where T : IComparable
  {
      T maximum = first;
      foreach (T item in values)
      {
          if (item.CompareTo(maximum) > 0)
          {
              maximum = item;
              }
      }
      return maximum;
  }
  public 
   static T Min<T>(T first, params T[] values)
      where T : IComparable
  {
      T minimum = first;

        foreach (T item in values)
        {
            if (item.CompareTo(minimum) < 0)
            {
               minimum = item;
            }
        }
      return minimum;
   }
}

You use the same syntax on a generic class when the method requires an additional type parameter not included in the class type parameter list. In this example, the method is static but C# does not require this.

Note that generic methods, like classes, can include more than one type parameter. The arity (the number of type parameters) is an additional distinguishing characteristic of a method signature.

Type Inferencing

The code used to call the Min<T> and Max<T> methods looks like that shown in Listing 11.34.

Example 11.34. Specifying the Type Parameter Explicitly

Console.WriteLine(
    MathEx.Max<int>(7, 490));
Console.WriteLine(
    MathEx.Min<string>("R.O.U.S.", "Fireswamp"));

The output to Listing 11.34 appears in Output 11.4.

Example 11.4.

490
Fireswamp

Not surprisingly, the type parameters, int and string, correspond to the actual types used in the generic method calls. However, specifying the type is redundant because the compiler can infer the type from the parameters passed to the method. To avoid redundancy, you can exclude the type parameters from the call. This is known as type inferencing, and an example appears in Listing 11.35. The output of this listing appears in Output 11.5.

Example 11.35. Inferring the Type Parameter

Console.WriteLine(
    MathEx.Max(7, 490));
Console.WriteLine(
    MathEx.Min("R.O.U.S'", "Fireswamp"));

Example 11.5.

490
Fireswamp

For type inferencing to be successful, the types must match the method signature. Calling the Max<T> method using MathEx.Max(7.0, 490), for example, causes a compile error. You can resolve the error by either casting explicitly or including the type argument. Also note that you cannot perform type inferencing purely on the return type. Parameters are required for type inferencing to be allowed.

Specifying Constraints

The generic method also allows constraints to be specified. For example, you can restrict a type parameter to implement IComparable. The constraint is specified immediately following the method header, prior to the curly braces of the method block, as shown in Listing 11.36.

Example 11.36. Specifying Constraints on Generic Methods


   public 
   class ConsoleTreeControl
{
    // Generic method Show<T>
    
   public 
   static 
   void Show<T>(BinaryTree<T> tree, int indent)
         
      where T : IComparable                              
    {
        Console.WriteLine("\n{0}{1}",
            "+ --".PadLeft(5*indent, ' '),
            tree.Item.ToString());
        if (tree.SubItems.First != null)
            Show(tree.SubItems.First, indent+1);
        if (tree.SubItems.Second != null)
            Show(tree.SubItems.Second, indent+1);
      }
}

Notice that the Show<T> implementation itself does not use the IComparable interface. Recall, however, that the BinaryTree<T> class did require this (see Listing 11.37).

Example 11.37. BinaryTree<T> Requiring IComparable Type Parameters


   public 
   class BinaryTree<T>
        
      where T: System.IComparable                         
{
    ...
}

Because the BinaryTree<T> class requires this constraint on T, and because Show<T> uses BinaryTree<T>, Show<T> also needs to supply the constraint.

Generic Internals

Given the discussions in earlier chapters about the prevalence of objects within the CLI type system, it is no surprise that generics are also objects. In fact, the type parameter on a generic class becomes metadata that the runtime uses to build appropriate classes when needed. Generics, therefore, support inheritance, polymorphism, and encapsulation. With generics, you can define methods, properties, fields, classes, interfaces, and delegates.

To achieve this, generics require support from the underlying runtime. So, the addition of generics to the C# language is a feature of both the compiler and the platform. To avoid boxing, for example, the implementation of generics is different for value-based type parameters than for generics with reference type parameters.

Instantiating Generics Based on Value Types

When a generic type is first constructed with a value type as a type parameter, the runtime creates a specialized generic type with the supplied type parameter(s) placed appropriately in the CIL. Therefore, the runtime creates new specialized generic types for each new parameter value type.

For example, suppose some code declared a Stack constructed of integers, as shown in Listing 11.41.

Example 11.41. Stack<int> Definition

Stack<int> stack;

When using this type, Stack<int>, for the first time, the runtime generates a specialized version of the Stack class with int substituted for its type parameter. From then on, whenever the code uses a Stack<int>, the runtime reuses the generated specialized Stack<int> class. In Listing 11.42, you declare two instances of a Stack<int>, both using the code already generated by the runtime for a Stack<int>.

Example 11.42. Declaring Variables of Type Stack<T>

Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();

If later in the code, you create another Stack with a different value type as its type parameter (such as a long or a user-defined struct), the runtime generates another version of the generic type. The benefit of specialized value type classes is better performance. Furthermore, the code is able to avoid conversions and boxing because each specialized generic class "natively" contains the value type.

Instantiating Generics Based on Reference Types

Generics work slightly differently for reference types. The first time a generic type is constructed with a reference type, the runtime creates a specialized generic type with object references substituted for type parameters in the CIL, not a specialized generic type based on the type parameter. Each subsequent time a constructed type is instantiated with a reference type parameter, the runtime reuses the previously generated version of the generic type, even if the reference type is different from the first reference type.

For example, suppose you have two reference types, a Customer class and an Order class, and you create an EntityDictionary of Customer types, like so:

 EntityDictionary<Guid, Customer> customers;

Prior to accessing this class, the runtime generates a specialized version of the EntityDictionary class that, instead of storing Customer as the specified data type, stores object references. Suppose the next line of code creates an EntityDictionary of another reference type, called Order:

EntityDictionary<Guid, Order> orders =
    new EntityDictionary<Guid, Order>();

Unlike value types, no new specialized version of the EntityDictionary class is created for the EntityDictionary that uses the Order type. Instead, an instance of the version of EntityDictionary that uses object references is instantiated and the orders variable is set to reference it.

To still gain the advantage of type safety, for each object reference substituted in place of the type parameter, an area of memory for an Order type is specifically allocated and the pointer is set to that memory reference. Suppose you then encountered a line of code to instantiate an EntityDictionary of a Customer type as follows:

 customers = new EntityDictionary<Guid, Customer>();

As with the previous use of the EntityDictionary class created with the Order type, another instance of the specialized EntityDictionary class (the one based on object references) is instantiated and the pointers contained therein are set to reference a Customer type specifically. This implementation of generics greatly reduces code bloat by reducing to one the number of specialized classes created by the compiler for generic classes of reference types.

Even though the runtime uses the same internal generic type definition when the type parameter on a generic reference type varies, this behavior is superseded if the type parameter is a value type. Dictionary<int, Customer>, Dictionary<Guid, Order>, and Dictionary<long, Order> will require new internal type definitions, for example.

Summary

Generics will significantly transform C# 1.0 coding style. In virtually all cases in which programmers used object within C# 1.0 code, generics would be a better choice in C# 2.0 to the extent that object should act as a flag for a possible generics implementation. The increased type safety, cast avoidance, and reduction of code bloat offer significant improvements. Similarly, where code traditionally used the System.Collections namespace, System.Collections.Generics should be selected instead.

The next chapter looks at one of the most pervasive generic namespaces, System.Collections.Generic. This namespace is composed almost exclusively of generic types. It provides clear examples of how some types that originally used objects were then converted to use generics.

800 East 96th Street, Indianapolis, Indiana 46240