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 ListListintegers = new List (); integers.Add(1); integers.Add(2); int extracted = integers[1];
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.Liststrings = new List (); strings.Add("Hello"); strings.Add("world"); string extracted = strings[1];
Dictionary
Another useful generic collection is DictionaryDictionaryitems = 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{ }
T _firstItem; T _secondItem; public T FirstItem { get { return _firstItem; } set { _firstItem = value; } } public T SecondItem { get { return _secondItem; } set { _secondItem = value; } }
public Pair(T firstItem, T secondItem) { FirstItem = firstItem; SecondItem = secondItem; }
public override string ToString() { return string.Format("{0} | {1}", _firstItem, _secondItem); }
Pairintegers = 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(); }
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