C# Tutorial Lesson 21: Generic Types [2.0]

Note: this lesson covers new functionality in C# 2.0, which at the time of writing is not released.

Constructed Types

Instances of generic types come in different flavours, and when you declare an instance you declare the flavour it has. To explain this in more concrete terms, let's take a look at the shortcomings of the otherwise useful System.Collections.ArrayList class.

An ArrayList is used in situations where we want an array-like list, but can't restrict the number of elements it may contain. For instance, suppose that we need to keep track of a list of names added into our application by the user. If the application is to support an indefinite number of added names, then we could use an ArrayList to store them. And, having stored them, the code to print out the list might look something like this:

1.

for (int x=0; x<arrList.Count; x++)

2.

    this.outputWindow.Text += (string) arrList[x];


Notice in the above that there is an explicit cast to a string when pulling out the string element from the ArrayList. This is because ArrayLists, in order to be of general use, store their elements as objects.

But this isn't an ideal situation when all you want to add to the ArrayList is strings. To make a runtime cast isn't very efficient, as it involves some background checks that the cast is valid. What would be better would be if the ArrayList could be declared as a string-only ArrayList, so that all type-safety checks could be run at compile time.

This is the kind of functionality provided by generics. When you declare a generic class you specify one or more 'type parameters' (which comprise the 'flavours' we appealed to at the start of this lesson). These type parameters then constrain the class instance in a way that compilers can verify.

Suppose that we wanted a generic version of the ArrayList class, which could be declared as interested only in strings. We might implement this using an internal array (though of course this may not be the best way to do it), and a partial specification of the class would look something like:

1.

public class ArrayList<T>

2.

{

3.

    private T[] itemArray;

4.

    

5.

    public void Add(T newItem)

6.

    {...}

7.

    

8.

    public T this[int i]

9.

    {...}

10.

}


Note how the type parameter T is included in angle brackets after the class name, and is used throughout the class definition wherever the definable type is needed.

An instance of this generic ArrayList class - what is called a (closed) constructed type - could then be declared and used with code like the following, which replaces the type parameters with type arguments:

1.

ArrayList<string> strArrList = new ArrayList<string>();

2.

strArrList.Add("fred");

3.

string str = strArrList[0];


After the constructed type has been set up, there is no need to cast the elements that are added into or removed from it; these have to be strings, and failure to comply with this requirement results in compile time errors.

Note also that it is not just standard classes that can take generic forms; there can also be generic structs, interfaces and delegates.

Multiple Type Parameters

The example class given above used only one type parameter. But a generic class can have any number of type parameters, which are separated both in the class definition and the instance declaration with commas inside the angle brackets. The declaration of such a class might look like this:

1.

public myGenericClass<T,U>

2.

{...}


All the examples in the draft literature use a capital letter to indicate a type parameter, so this usage should be taken as good practice. However, since type parameters are named using standard identifiers, there is no formal requirement to use capital letters (this is also an implication, of course, of the fact that there is no limit to the number of type parameters a class may have).

It is possible to have classes with the same name but different numbers of class parameters (note that it is the number of class parameters, not their identifiers that is important). So, for instance, you could have both the following classes declared within the same namespace:

1.

public myGenericClass<T>

2.

public myGenericClass<T,U>


but not these:

1.

public myGenericClass<T,U>

2.

public myGenericClass<V,W>


Generic Methods

Standard, non-generic classes can have generic methods, which are methods that are declared with type parameters. Both the inputs and the outputs of these methods may reference the type variables, allowing code such as:

1.

public T MyMethod<T>(myGenericClass<T>) {}


Overloading occurs on methods analogously to the way it occurs on classes; the number of type parameters a method has is used to distinguish it from other methods.

It is possible to call generic methods without actually giving a type argument; this relies upon a process of type inference. For example, the method given above could be called using code like:

1.

myGenericClass<int> myG = new myGenericClass<int>();

2.

int i = MyMethod(myG);


Type inference involves the compiler working out which type argument must have been meant given the way the method was invoked. It seems dubious to us, however, that the brevity it provides outweighs the clarity of leaving in the type argument.

Note that while there are generic forms of methods, there are no generic forms of properties, nor of events, indexers and operators.

Type Constraints

Until now, we have implicitly assumed that any type argument may be provided for any type parameter. But it is possible to restrict the range of possible values for each type parameter by specifying constraints.

The following code comprises the header of a definition for the generic class myGenericClass. The two 'where' clauses (which are placed on separate lines for readability only) provide the constraints. The first clause restricts the first type parameter T to types which are - or which sub-class - the myConstraint class. The second clause extends this constraint on U to myConstraint types which also satisfy the myInterface interface.

1.

public class myGenericClass<T,U>

2.

    where T: myConstraint

3.

    where U: myConstraint, myInterface

4.

    {...}


Note that a single constraint can mention any number of interfaces, but a maximum of one class.

Why might we want to place a constraint on a type parameter? Well, suppose that we wanted to implement a bubble-sort routine in the 'Sort' method for our generic ArrayList class. In this case it would be useful to restrict the types we're dealing with to those which implement the IComparable interface (which ensures that any two elements of the same type can be ranked). This allows us to use the 'CompareTo' method without having to make any runtime casts or trap errors, as shown by the following code.

1.

public class ArrayList<T> where T: IComparable

2.

{

3.

    private T[] itemArray;

4.

    public void Sort()

5.

    {

6.

        for (int x=1; x<itemArray.Length; x++)

7.

        {

8.

            for (int y=0; y<x; y++)

9.

            {

10.

                int z = x-y;

11.

                T itemHigher = itemArray[z];

12.

                T itemLower = itemArray[z-1];

13.

                if (itemLower.CompareTo(itemHigher)>0)

14.

                {

15.

                    itemArray[z] = itemLower;

16.

                    itemArray[z-1] = itemHigher;

17.

                }

18.

            }

19.

        }

20.

    }

21.

}


Link Building Information