CHAPTER 17 Generics in C#

Without a doubt, generics are the biggest version 2.0 addition to the C# language. One of the biggest disappointments of the C# designers was their inability to add generic types (also sometimes known as parameterized types) to the first version of the language. The motivation for generics is simple—suppose a class wants to store a member variable, or a method wants to take a parameter, but the class of the input object should be specified by the client of the class or method, not by the author of the original code. Without generics, the only way to accom­plish this is by providing the loosest possible typing, which means using object as the class for the member variable or method parameter. This approach has two problems: the lack of type- safety and the performance problems caused by boxing. Using generics solves these problems.

1. An Overview of Generics

For programmers coming to C# version 1 from a C++ background, the first feature request they often came up with was for generics to be included in the C# language. The prototypical use for generics is collection class libraries, but this powerful language feature can also be used in many other ways to build useful classes that would be difficult or impractical without generics.

C# generics differ from C++ templates in a number of ways. The key difference is that C# generics aren’t a language-only feature and have explicit runtime support. This means other languages that support generics, such as Visual Basic .NET and C++/CLI, can consume a generic class written in C#. Generics don’t support some of the more advanced features of C++ templates, such as partial template specification. In addition, C++ templates are a compile-time feature, while .NET generics are “expanded” at runtime.

Consider the following code sample that implements a growable array, written without using generics:

public class GrowableArray {

private object[] collection = new object[l6];

private int count = 0;

public void AddElement(object element)

{

if (count >= collection.Length)

{

object[] temp = new object[collection.Length * 2];

Array.Copy(collection, temp, collection.Length);

collection = temp;

}

collection[count] = element;

++count;

}

public object GetElement(int elementNumber)

{

if (elementNumber >= count)

{

throw new IndexOutOfRangeException();

}

return collection[elementNumber];

}

}

Using an internal object array allows any type to be used within the collection; however, as mentioned in Chapter 9, storing a value type using an object reference causes a boxing operation to occur. The return type of the GetElement method demonstrates the other problem with object-based collections—a cast will generally be required to convert the obj ect reference to the specific type stored in the collection. The following code highlights these issues:

GrowableArray ga = new GrowableArray();

int num = 10;

ga.AddElement(num); //boxing operation here

//unboxing operation and no compile-time type-safety

int newNum = (int)ga.GetElement(0);

//runtime error here

string str = (string)ga.GetElement(0);

Generics solve the problems shown in the previous code sample. Instead of using an object reference to specify a “generic” parameter, generics allow the type of a parameter to be left unspecified until the generic code is used. You implement this by specifying a special parameter known as a type parameter inside angle brackets (< and >); type parameters signify that it’s up to the client code to provide the actual type that will be substituted for the type parameter. If you rewrite the earlier GrowableArray sample using generics, you simply have to replace object references with the generic placeholder T:

public class GrowableArray<T>

{

private T[] collection = new T[16]; private int count = 0;

public void AddElement(T element)

{

if (count >= collection.Length)

{

T[] temp = new T[collection.Length * 2];

Array.Copy(collection, temp, collection.Length);

collection = temp;

}

collection[count] = element;

++count;

}

public T GetElement(int elementNumber)

{

if (elementNumber >= count)

{

throw new IndexOutOfRangeException();

}

return collection[elementNumber];

}

}

In this GrowableArray collection, a type parameter called T has been specified at the class level, and all the code that deals with the internal array has been converted from referencing obj ect to referencing T. The author of the collection doesn’t know the exact type of T, and code that uses the collection can specify any type that it wants to store in the collection. Code that uses the generic GrowableArray now looks like this:

GrowableArray<int> ga = new GrowableArray<int>();

int num = 10;

ga.AddElement(num); //no boxing operation here

int newNum = ga.GetElement(0);

//type-safety and no unboxing operation

// string str = (string)ga.GetElement(0);

//would not compile

GrowableArray<int> is known as a constructed type, and the type that the consumer of the generic collection supplies to be substituted with the generic type is known as the type argument. Constructed types that use different type arguments but are based on the same generic type aren’t equivalent. For example, an object of type GrowableArray<int> couldn’t be passed to a method if a GrowableArray<string> was expected. Constructed types can’t be substituted even if an implicit conversion exists between the type arguments. The following code sample won’t compile, despite the implicit cast available from int to double and object:

public void MethodOne(GrowableArray<double> gad)

{

;

}

public void MethodTwo(GrowableArray<object> gad)

{

;

}

static void main()

{

GrowableArray<int> ga = new GrowableArray<int>();

MethodOne(ga); //will NOT compile

MethodTwo(ga); //will NOT compile

}

This restriction applies only to the constructed type, not to the type arguments. If an implicit conversion exists between the type of a variable and the type of the type argument, conversion will be successful as expected. This means the following code is legal:

GrowableArray<double> ga = new GrowableArray<double>();

int i = 7;

ga.AddElement(i); //the variable i is implicitly converted to a double

A generic type may contain any number of type parameters. An associative collection such as a dictionary or map, where a value is associated with a particular key, is a common use of multiple type parameters, because the type of the key and type of the value will typically be different. You should separate type parameters using commas within the angle brackets:

public class Dictionary<T, K> {}

You can declare a constructed type of a generic type with multiple type parameters by providing a comma-delimited list of type arguments:

Dictionary<string, int> dictionaryOfStringToInt = new Dictionary<string, int>();

2. Constraints

In all the examples up to this point, variables that are instances of type parameters haven’t had methods called upon them and new instances haven’t been constructed. For generic classes that are simple containers or that implement simple algorithms, this limitation is fine; however, for many real-world cases, variables of generic types need to be created and have methods called upon them inside the generic class. Casting the variable represented by a type parameter is an option, but this defeats the compile-time type-checking benefits that are such a large part of generics. To overcome this problem, type parameters can have one or more constraints that define the structural qualities a type must exhibit to be an eligible argument for a particular template parameter.

Constraints can specify the following:

  • Whether a type argument is a class or a struct
  • A number of interfaces that the argument must implement
  • A class that the type must be of or must derive from
  • The existence of a default constructor

.Each type parameter can have a different set of constraints, and all constraints are specified fter the type parameter list using a where clause. In the following sample, both type parameters have constraints; the template parameter T is required to be a class that implements IConvertible and provides a default constructor, and K must be a struct:

public class Container<T, K>

where T: class, IConvertible, new()

where K: struct

{

public Container()

{

T t = new T();

int i = t.ToInt32(null);

K k = new K();

}

}

In the constructor, the type parameters declare local variables, and the variable of the generic type T has the ToInt32 method of IConvertible called upon it. Type parameter K was constrained to be a struct, which means it will act as though it has a parameterless constructor, so new instances of K can be created without the new constraint being specified. It’s an error to apply the new constraint to a struct, and a number of other invalid constraints aren’t legal, such as the provision of multiple class constraints and the use of class constraints that specify a sealed class.

Constraints must appear in a certain order. If a type parameter is going to be constrained to a class, struct, or particular subclass, this constraint must appear first. Any number of interface constraints can then appear, and the final optional constraint is the constructor constraint.

3. Generic Methods

It’s sometimes desirable for generics to be utilized at the method level rather than a type level. This could be the case for a class that exposes procedural methods or where the generic type is relevant only to a specific function. In these cases, you can define the generic template entirely at the function level, and the class that the generic method is defined in doesn’t need to be generic:

public class Utils {

Random _random = new Random();

public T ReturnRandomElementOfArray<T>(T[] coll)

{

return coll[_random.Next(coll.Length)];

}

}

In this example, a utility class has a method that returns a random element of an array, and by using a generic method, you can realize the benefits of type-safety and avoid boxing. You can call the method by specifying the type argument in angled brackets after the method name; alternatively, you can omit the type argument, and the code can rely on compiler-provided inference to determine the value of the type argument. The following code shows both cases:

Utils u = new Utils();

double[] dblColl = new double[]{1.0, 2.0, 3.0};

int[] intColl = new int[]{4, 5, 6};

//type argument specified

double dblRandom = u.ReturnRandomElementOfArray<double>(dblColl);

//type argument inferred

int intRandom = u.ReturnRandomElementOfArray(intColl);

Relying on type argument inference may make the calling code become more compact and appear more natural, but you can receive some unexpected consequences in the face of overloading and version changes. If you updated the Utils class shown in the previous example to include a nongeneric version of ReturnRandomElementOfArray that took an int array and returned a single int, the code in the previous sample that relies on type argument inference would switch to the nongeneric method when recompiled against the newer version. Although this is techni­cally the correct behavior on the part of the compiler, it may surprise you to have a different method called in the absence of modifications in your source code. It’s therefore a good idea to avoid overloading generic and nongeneric methods when possible.

You can apply the same constraints to generic methods as you do to nongeneric methods. When a generic method is overridden in a derived class, any constraints on the type parameters are inherited as well and can’t be repeated in the overridden method. The following code illustrates a number of these scenarios. Note that the type parameter name doesn’t need to be constant and can be changed in derived classes. Unless a class is inheriting a generic class and imple­menting a generic interface (covered in a moment), changing the name of a type parameter isn’t recommended.

//generic base class with a generic method public class GenericBase<T>

{

public virtual void MyMethodUsingGenericParameter(T t) { }

public virtual void MyGenericMethod<W>(W w) where W: IComparable{ }

}

//derived generic class

public class GenericInherited<V>: GenericBase<V>

{

public override void MyMethodUsingGenericParameter(V v) { }

public override void MyGenericMethod<W>(W w) { } //IComparable constraint

//is inherited from GenericBase

}

//nongeneric class

public class NonGenericInherited : GenericInherited<int>

{

public override void MyMethodUsingGenericParameter(int i) { }

public override void MyGenericMethod<W>(W w) { }

}

4. Inheritance, Overriding, and Overloading

Generics and inheritance can mix in interesting ways. The first point to note is that a generic parameter can’t act as the base class for a generic type (regardless of any constraints such as class), meaning the following code isn’t legal:

public class WillNotCompile<T> : T { }

// error CS0689: Cannot derive from ‘T’ because it is a type parameter

This prohibits the popular “mix-in” style of generic programming. Although this capability isn’t allowed in C# 2.0, the C# design team understands the utility of such constructs, and it may appear in future versions of the language.

Other than this exclusion, inheritance and generics have few scenarios that aren’t permitted. All of the following scenarios are legal in C#:

  • A generic class can have a nongeneric class as a base.
  • A nongeneric class can have a constructed generic class as a base if all type arguments are provided.
  • A generic class can have a generic class as a base. If there are constraints on the type parameters in the base class, these must be repeated with the derived class; the require­ment to repeat the constraints is in contrast to the implicit inheritance of method-level constraints.
  • If a generic class has multiple generic parameters, a base class can provide type arguments for zero or more of the type parameters.

The following sample demonstrates the first two scenarios:

//base – a nongeneric class

public class BaseClass { }

//first tier – a derived generic class

public class DerivedGenericClass<T>: BaseClass {}

//second tier – a derived generic class and a derived nongeneric class

public class DerivedClass: DerivedGenericClass<int> {}

public class SecondDerivedGenericClass <T>: DerivedGenericClass<T> { }

The following sample shows the third scenario. If the constraint isn’t included in the derived generic class, it isn’t legal.

//constraints repeated in derived generic class

public class GenericBase<T> where T : new() {}

public class GenericDerived<T> : GenericBase<T> where T : new() { }

Finally, the following code shows the fourth scenario where a nongeneric type is derived from a generic type in two inheritance steps:

public class Dictionary<T, K> { }

public class StringDictionary<K> : Dictionary<string, K> { }

public class StringToIntDictionary: StringDictionary<int> { }

public class StringToDoubleDictionary : Dictionary<string, double> { }

5. Generic Interfaces, Delegates, and Events

Interfaces can have type parameters in much the same way as classes. The only major restric­tion is that a particular generic interface can appear only once in a particular class hierarchy, which means a class can’t implement the same generic interface twice with a different type argument. The following code shows permitted interface implementation scenarios, with the invalid implementation commented out:

public interface IGeneric<T>

{

T InterfaceMethod();

}

public class GenericInterfaceImplementor<T>: IGeneric<T>

{

public T InterfaceMethod() { return default(T); }

}

public class InterfaceImplementor: IGeneric<int>

{

public int InterfaceMethod() { return 0; }

}

/* compile error CS0111: Type ‘DerivedInterfaceImplementor’ already

defines a member called

‘InterfaceMethod’ with the same parameter types

public class DerivedInterfaceImplementor : InterfaceImplementor, IGeneric<double>

{

public int InterfaceMethod() { return 0; }

public double InterfaceMethod() { return 0.0; }

}

*/

Note the use of the default keyword in GenericInterfaceImplementor.InterfaceMethod. You can use the default keyword to assign or compare an instance of a parameterized type without needing to know or specify whether the parameterized type is a class or a struct. For classes, default is the same as null and the default value for value types. In the case of structs, the member variables of a default instance follow the rules for classes and primitives.

Delegates support generics using a pattern similar to generic methods. A delegate can use a type parameter in a generic class, or a generic delegate can be declared within a nongeneric class. In the case of the latter, you can express constraints with the delegate declaration:

public class GenericClass <T>

{

public delegate T GenericDelegate(int i);

public void UseDelegate(GenericDelegate del) {}

}

public class NonGeneric {

public delegate T GenericDelegate1<T>(int i) where T : class;

public delegate U GenericDelegate2<U, V>(V v);

public void UseDelegate<T>(GenericDelegate1<T> del) where T : class { }

}

In the example, the type parameter and constraint for T appears in GenericDelegate1 and UseDelegate. Notice that when a generic delegate is declared on a nongeneric class, the type parameters and constraints must be repeated in every declaration that uses the type parameter. This is the same as the situation that exists with generic methods on a nongeneric class. The only case in which the constraint doesn’t need to be repeated is with overridden generic methods.

C# allows the full definition of type arguments when an instance of a delegate is declared, or the programmer can rely on the compiler to infer type arguments. Using the delegate methods shown in the previous sample, all the following forms of delegate instantiation are supported:

string StandardMethod(int i) { return “”; }

public void UseGenericDelegates()

{

GenericClass<string> gcs = new GenericClass<string>();

//generic method can be called used explicitly like this

GenericClass<string>.GenericDelegate del = new

GenericClass<string>.GenericDelegate(StandardMethod);

gcs.UseDelegate(del);

//or implicitly like this

gcs.UseDelegate(StandardMethod);

//implicit use of generic delegate

NonGeneric ng = new NonGeneric();

ng.UseDelegate<string>(StandardMethod);

For a delegate declared outside a class, the option of using the containing class’s type parameters is obviously unavailable, and type parameters and any constraints must be declared with the delegate.

You can associate a generic delegate with events. This can be a particularly useful technique, as it allows a single delegate to be associated with multiple events. Each event can specify custom type arguments, allowing strong typing to be achieved without the need to specify a new delegate signature. The following sample shows a generic delegate that’s associated with an event; this raises events that provide information about the time that event was raised in the EventArgs-derived argument:

//generic delegate that takes sender and event args type parameters public delegate void StandaloneGenericDelegate<S, A>(S sender, A args);

public class TimedEventArgs: EventArgs {

//constructor

public TimedEventArgs(DateTime time) { _timeOfEvent = time; }

//member variable DateTime _timeOfEvent;

//property

public DateTime TimeOfEvent { get{ return _timeOfEvent; }}

}

public class TimedEventRaiser

{

public event StandaloneGenericDelegate<TimedEventRaiser, TimedEventArgs>

TimedEvent;

public void Raiser()

{

if (TimedEvent != null)

{

TimedEvent(this, new TimedEventArgs(DateTime.Now));

}

}

}

6. Conclusion and Design Guidance

Generics are a welcome addition to the C# language. They provide two key features: compile­time type-safety and the elimination of boxing and unboxing operations for value types. Generics fall into the group of language elements that initially appear rather bland, utilitarian, and limited in scope, but they allow powerful code patterns and elegant designs that are difficult to achieve in their absence.

The real design clue that indicates generics are required is loosely typed declarations.

If parameters or return types are declared as object even though it feels like stronger typing would be better, it’s generally a good time to introduce generics. Feeling a desire for stronger typing is often associated with the requirement to perform a significant number of casting operations. So, to make this advice a bit more concrete, if there’s a couple of casts (or more) required to perform a particular logical operation, such as opening a database connection, it’s a good indication you should use generics.

As with any new language or developer tool feature, there can be a tendency to overuse the “new toy” to solve every coding problem. And as with all new toys, the greatest joy and satisfac­tion may come from using the toy in unanticipated and novel ways, but be wary of overuse that can come from the “everything looks like a nail when my only tool is a hammer” pattern of thought.

A common question about generic constraints is, why are they so limited in what they support?

Although it’s possible to provide a more extensive constraint syntax, it’s not clear at this point where the ultimate “sweet spot” is for such support, so it was decided to “step lightly” in this version, with the possibility for more support in future versions of the language.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *