Aug 13, 2010

What are Generics?

Generic Types

Microsoft introduced generics to the .NET framework with version 2.0. Generic programming allows highly reusable classes to be created without specifying the types that they operate upon. These types are only provided when the class is used.

What are Generics?

The .NET framework version 1.1 supported the creation of various types of collection. These collections allow a number of objects to be gathered together in various structures, such as simple lists, hash tables, stacks and queues. One of the key drawbacks with all of the .NET 1.1 collections is that they are not type-safe. This means that any value or object can be added to a collection, with all reference types being cast as objects and value types being boxed.
The lack of type safety can cause logical problems. For example, if you have a collection that you are using solely to hold a group of "Customer" objects, it is possible to accidentally add an "Order" object to the list. If you later attempt to read the Order object back from the collection and convert it to a Customer, the conversion will fail and cause a run-time exception. Additionally, the casting or boxing of all items in the collection to hold them as objects adds a processing overhead, as does unboxing or casting back to the original type. This overhead can cause performance issues.
To overcome these problems in a .NET 1.1 project, it was common to manually code type-safe collections. In the case of the Customer object, a "CustomerCollection" class with all of the necessary methods, properties and events would be created. This would remove the processing overheads of casting and ensure that if an attempt were made to add the incorrect type of object, it would be caught with a compiler error. Unfortunately the capacity for reuse of the CustomerCollection code would be limited. If a similar collection were required to hold Orders, it would need to be created separately, even though the code would be similar.
With the .NET version 2.0, Microsoft introduced Generics. Generic programming allows type-safe classes and methods to be created without specifying the types that they operate on. The types are declared, using type parameters, only when the class is used, allowing different instances to work with different types. This overcomes both of the previously stated problems. As the classes are type-safe, there is no requirement for boxing or casting when reading from or writing to a collection. This leads to performance improvements of 100% or more. As the types are set when a generic class is instantiated, the code can be reused, minimising duplication and increasing developer productivity.

Using Generic Types

Many generic types are included in the .NET framework. These include some generic collections that solve the problems described earlier without the requirement to write any extra code. You can find some of these in the System.Collections.Generic namespace. To follow the examples in the article, add the following using directive to your code:
using System.Collections.Generic;

List

To demonstrate the use of a generic type, we can use the List class, spoken as "List of T". This is similar to the ArrayList class in that it provides a simple collection of objects that can be accessed using an index number. The important part to note is the "T" element. This is the type parameter that determines the type of objects or values that may be held in the collection. For example, the following code creates a List of integers; hence the collection is declared using "List".
List integers = new List();
integers.Add(1);
integers.Add(2);
int extracted = integers[1];
Unlike with an ArrayList, the final line of this sample does not need to unbox the integer into the extracted variable. This improves the performance. To prove the type-safety of the collection, try adding the following line, which attempts to add a floating-point value to the list. Instead of allowing this and potentially causing a problem at run-time, the code will fail to compile.
integers.Add(3.4);
The key benefit of a generic class is its ability to be reused for another type. Try running the following code, which is almost identical to the first example except for the use of strings instead of integers.
List strings = new List();
strings.Add("Hello");
strings.Add("world");
string extracted = strings[1];

Dictionary

Another useful generic collection is Dictionary. This is similar to the Hashtable class, in that it stores a key and a value for each element in the collection and allows a value to be retrieved quickly using the key as a lookup. It also demonstrates that a generic type may include more than one type parameter. In this case, the key types and value types are declared using the TKey and TValue type parameters respectively. In the example below, integers are used for the keys and strings for the values.
Dictionary items = new Dictionary();
items.Add(50, "Hello");
items.Add(99, "world");
string extracted = items[99];

Creating a Generic Class

To create a generic class, you simply add one or more type parameters to the class definition. When using several type parameters, separate them with commas. Type parameters are often named with a single, upper case letter. However, this is not a requirement and names can be made longer and more meaningful.
In this section we will create a simple generic class that holds a pair of values or objects. The held items will be of a type specified when the class is used so we will use a type parameter, in this case named "T". To declare the class, add the following code:
public class Pair
{
}
Whenever you need to use a value that is of the undeclared type, it can now be referenced using T. For example, to add the properties that hold the two values, add the following code. Note that the types of the properties and their backing stores use the unspecified type T.
T _firstItem;
T _secondItem;

public T FirstItem 
{
    get { return _firstItem; }
    set { _firstItem = value; }
}

public T SecondItem
{
    get { return _secondItem; }
    set { _secondItem = value; }
}
When creating a constructor for a generic type, you can use the type parameter to define the types of incoming parameters where required.
public Pair(T firstItem, T secondItem)
{
    FirstItem = firstItem;
    SecondItem = secondItem;
}
Finally, we can add a method to the class that we will use to show the contents of the Pair. The following overrides to ToString method to combine and return the values:
public override string ToString()
{
    return string.Format("{0} | {1}", _firstItem, _secondItem);
}
You can now use the Pair class with any type. The sample below shows the results of Pair instances containing integers and strings.
Pair integers = new Pair(1, 2);
Console.WriteLine(integers);

Pair strings = new Pair("Hello", "world");
Console.WriteLine(strings);

/* OUTPUT

1 | 2
Hello | world

*/

Generic Constraints

The Pair class is a useful example of a generic type. However, it has one key problem. If you use Intellisense in Visual Studio you may have noticed that the "T" type has very few available members and that these are the methods and properties of a basic object. This means that T may represent any object but that the operations available to those objects are limited. You can overcome this by casting the T objects to other types but if you do, the type safety is lost. You could inadvertently create an instance of Pair for a type that does not support this cast and cause run-time exceptions.
When you need access to other members of a generic type, you can apply generic constraints to a type parameter. These constraints ensure that a class or structure that does not meet your requirements cannot be used for a type parameter.
There are three types of generic constraint. These are:
  • Derivation constraints.
  • Default constructor constraints.
  • Value / reference type constraints.

Derivation Constraints

A derivation constraint specifies that the type specified for a type parameter must derive from a supplied base class and / or implement one or more specified interfaces. When a derivation constraint is applied, all of the members of the provided base class and interfaces can be used for all objects of the type parameter. For example, if the Pair class needed access to the Dispose method provided by IDisposable, adding a derivation constraint for IDisposable would make the Dispose method be available and be displayed in Intellisense.
To add a derivation constraint to a type parameter, you use the following syntax:
class class-name where T : base-class, interface-1, ..., interface-X
The base-class element is optional and is used when you want the type parameter to be a subclass of a specified base class. As classes may only inherit from one base class, only one base class can be specified here and must be the first item in the list. Any number of interfaces may be included. Note that the types used in the generic class must inherit from any base class specified and must implement all listed interfaces. If you are using several type parameters, each may include its own constraints.
To ensure that the Pair class is used with types that implement IDisposable, the declaration can be updated as follows:
public class Pair where T : IDisposable
The T type now includes the members of the IDisposable interface, permitting an additional method that calls Dispose to be included in the class:
public void DisposeItems()
{
    FirstItem.Dispose();
    SecondItem.Dispose();
}
You will find that the test code that uses the Pair class is no longer valid as the int and string types do not implement IDisposable. Try using a type that implements IDisposable before removing the constraint and the DisposeItems method.

Default Constructor Constraints

If you need to instantiate a new object of the generic type specified by a type parameter within a generic class, you can apply a default constructor constraint. This type of constraint prevents the class from being instantiated for classes that do not include a public parameterless constructor. To apply such a constraint to the Pair class, modify the class' declaration as follows:
public class Pair where T : new()
You could now include methods that create new T instances, such as:
public T InstantiateNew()
{
    return new T();
}

Value / Reference Type Constraints

The third type of constraint is the value / reference type constraint. This type of constraint ensures that type parameters are always either value types or reference types. To force the Pair class to only accept reference types, the following definition could be used:
public class Pair where T : class
To ensure that the type parameter is always a non-nullable value type, the following definition would be used:
public class Pair where T : struct

No comments:

Post a Comment