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.