Essential C# 2.0

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.

Listing 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.

Listing 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.

Listing 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 errorspecifically, an InvalidCastException. This defeats an advantage of generics.

Language Contrast: C++Templates

Generics in C# and the CLR differ from similar constructs in other languages. While other languages provide similar functionality, C# is significantly more type safe. Generics in C# is a language feature and a platform feature, the underlying 2.0 runtime contains deep support for generics in its engine.

C++ templates differ significantly from C# generics, because C# takes advantage of the CIL. C# generics are compiled into CIL, causing specialization to occur at execution time for each value type only when it is used, and only once for reference types.

A distinct feature not supported by C++ templates is explicit constraints. C++ templates allow you to compile a method call that may or may not belong to the type parameter. As a result, if the member does not exist in the type parameter, an error occurs, likely with a cryptic error message and referring to an unexpected location in the source code. However, the advantage of the C++ implementation is that operators (+, -, and so on) may be called on the type. C# does not support the calling of operators on the type parameter because operators are staticso they can't be identified by interfaces or base class constraints.

The problem with the error is that it occurs only when using the template, not when defining it. Because C# generics can declare constraints, the compiler can prevent such errors when defining the generic, thereby identifying invalid assumptions sooner. Furthermore, when declaring a variable of a generic type, the error will point to the declaration of the variable, not to the location in the generic implementation where the member is used.

It is interesting to note that Microsoft's CLI support in C++ includes both generics and C++ templates because of the distinct characteristics of each.

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.

Listing 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.

Output 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.

Listing 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.

Listing 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.

Listing 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.

Listing 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.

Listing 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.

Advanced Topic: Constraint Limitations

Constraints are appropriately limited to avoid nonsense code. For example, you cannot combine a base class constraint with a struct or class constraint, nor can you use Nullable<T> on struct constraint type parameters. Also, you cannot specify constraints to restrict inheritance to special types such as object, arrays, System.ValueType, System.Enum (enum),System.Delegate, and System.MulticastDelegate.

In some cases, constraint limitations are perhaps more desirable, but they still are not supported. The following subsections provide some additional examples of constraints that are not allowed.

Operator Constraints Are Not Allowed

Another restriction on constraints is that you cannot specify a constraint that a class supports on a particular method or operator, unless that method or operator is on an interface. Because of this, the generic Add() in Listing 11.27 does not work.

Listing 11.27. Constraint Expressions Cannot Require Operators

public abstract class MathEx<T> { public static T Add(T first, T second) { // Error: Operator '+' cannot be applied to // operands of type 'T' and 'T'. return first + second; } }

In this case, the method assumes that the + operator is available on all types. However, because all types support only the methods of object (which does not include the + operator), an error occurs. Unfortunately, there is no way to specify the + operator within a constraint; therefore, creating an add method like this is a lot more cumbersome. One reason for this limitation is that there is no way to constrain a type to have a static method. You cannot, for example, specify static methods on an interface.

OR Criteria Are Not Supported

If you supply multiple interfaces or class constraints for a type parameter, the compiler always assumes an AND relationship between constraints. For example, where TKey : IComparable, IFormattable requires that both IComparable and IFormattable are supported. There is no way to specify an OR relationship between constraints. Hence, an equivalent of Listing 11.28 is not supported.

Listing 11.28. Combining Constraints Using an OR Relationship Is Not Allowed

public class BinaryTree<T> // Error: OR is not supported. where T: System.IComparable || System.IFormattable<T> { ... }

Supporting this would prevent the compiler from resolving which method to call at compile time.

Constraints of Type Delegate and Enum Are Not Valid

Readers who are already familiar with C# 1.0 and are reading this chapter to learn 2.0 features will be familiar with the concept of delegates, which are covered in Chapter 13. One additional constraint that is not allowed is the use of any delegate type as a class constraint. For example, the compiler will output an error for the class declaration in Listing 11.29.

Listing 11.29. Inheritance Constraints Cannot Be of Type System.Delegate

// Error: Constraint cannot be special class 'System.Delegate' public class Publisher<T> where T : System.Delegate { public event T Event; public void Publish() { if (Event != null) { Event(this, new EventArgs()); } } }

All delegate types are considered special classes that cannot be specified as type parameters. Doing so would prevent compile-time validation on the call to Event() because the signature of the event firing is unknown with the data types System.Delegate and System.MulticastDelegate. The same restriction occurs for any enum type.

Constructor Constraints Are Allowed Only for Default Constructors

Listing 11.25 includes a constructor constraint that forces TValue to support a default constructor. There is no constraint to force TValue to support a constructor other than the default. For example, it is not possible to make EntityBase.Key protected and only set it in a TValue constructor that takes a TKey parameter using constraints alone. Listing 11.30 demonstrates the invalid code.

Listing 11.30. Constructor Constraints Can Be Specified Only for Default Constructors

public TValue New(TKey key) { // Error: 'TValue': Cannot provide arguments // when creating an instance of a variable type. TValue newEntity = null; // newEntity = new TValue(key); Add(newEntity.Key, newEntity); return newEntity; }

One way to circumvent this restriction is to supply a factory interface that includes a method for instantiating the type. The factory implementing the interface takes responsibility for instantiating the entity rather than the EntityDictionary itself (see Listing 11.31).

Listing 11.31. Using a Factory Interface in Place of a Constructor Constraint

[View full width]

public class EntityBase<TKey> { public EntityBase(TKey key) { Key = key; } public TKey Key { get { return _key; } set { _key = value; } } private TKey _key; } public class EntityDictionary<TKey, TValue, TFactory> : Dictionary<TKey, TValue> where TKey : IComparable, IFormattable where TValue : EntityBase<TKey> where TFactory : IEntityFactory<TKey, TValue>, new() { ... public TValue New(TKey key) { TValue newEntity = new TFactory().CreateNew(key); Add(newEntity.Key, newEntity); return newEntity; } ... } public interface IEntityFactory<TKey, TValue> { TValue CreateNew(TKey key); } ...

A declaration such as this allows you to pass the new key to a TValue constructor that takes parameters rather than the default constructor. It no longer uses the constructor constraint on TValue because TFactory is responsible for instantiating the order instead of EntityDictionary<...>. (One modification to the code in Listing 11.31 would be to save a copy of the factory. This would enable you to reuse the factory instead of reinstantiating it every time.)

A declaration for a variable of type EntityDictionary<TKey, TValue, TFactory> would result in an entity declaration similar to the Order entity in Listing 11.32.

Listing 11.32. Declaring an Entity to Be Used in EntityDictionary<...>

public class Order : EntityBase<Guid> { public Order(Guid key) : base(key) { // ... } } public class OrderFactory : IEntityFactory<Guid, Order> { public Order CreateNew(Guid key) { return new Order(key); } }

Категории