Generic Constraints


With C# generics, the compiler compiles the generic code into IL independent of any specific types the client will use. As a result, the generic code could try to use methods, properties, or members of the generic type parameters that are incompatible with the specific types the client uses. This is unacceptable, because it amounts to a lack of type safety. In C#, you need to instruct the compiler which constraints the client-specified types must obey in order for them to be used instead of the generic type parameters. There are three types of constraints, which the following sections will explore in detail:


Derivation constraint

Indicates to the compiler that the generic type parameter derives from a base type such as an interface or a particular base class


Default constructor constraint

Indicates to the compiler that the generic type parameter exposes a default public constructor (a public constructor with no parameters)


Value/reference type constraint

Constrains the generic type parameter to be a value or a reference type

A generic type can employ multiple constraints, and you even get IntelliSense reflecting the constraints when using the generic type parameter (e.g., suggesting methods or members from the base type).

Note that although constraints are optional, they are often essential when developing a generic type. Without them, the compiler takes the more conservative, type-safe approach and allows access only to object-level functionality in your generic type parameters. Constraints are part of the generic type metadata, so the client-side compiler can take advantage of them as well. The client-side compiler allows the client developer to use only types that comply with the constraints, thus enforcing type safety.

An example will go a long way toward explaining the need for and use of constraints. Suppose you would like to add indexing or searching by key abilities to the linked list of Example D-3:

     public class LinkedList<K,T>     {        T Find(K key)        {...}        public T this[K key]        {           get           {              return Find(key);           }        }     } 

This allows the client to write the following code:

     LinkedList<int,string> list = new LinkedList<int,string>( );     list.AddHead(123,"AAA");     list.AddHead(456,"BBB");     string item = list[456];     Debug.Assert(item == "BBB"); 

To implement the search, you need to scan the list, compare each node's key with the key you're looking for, and return the item of the node whose key matches. The problem is that the following implementation of Find( ) does not compile:

     T Find(K key)     {        Node<K,T> current = m_Head;        while(current.NextNode != null)        {           if(current.Key == key) //Will not compile           {              break;           }           else           {                     current = current.NextNode;           }        }        return current.Item;     } 

The compiler will refuse to compile this line:

     if(current.Key == key) 

because the compiler does not know whether K (or the actual type supplied by the client) supports the == operator. For example, structs do not provide such an implementation by default. You could try to overcome the == operator limitation by using the IComparable interface:

     public interface IComparable     {        int CompareTo(object obj);     } 

CompareTo( ) returns 0 if the object you compare to is equal to the object implementing the interface, so the Find( ) method could use it as follows:

     if(current.Key.CompareTo(key) == 0) 

Unfortunately, however, this does not compile either, because the compiler has no way of knowing whether K (or the actual type supplied by the client) is derived from IComparable.

You could explicitly cast to IComparable to force the compiler to compile the comparing line, but you would do so at the expense of type safety:

     if(((IComparable)(current.Key)).CompareTo(key) == 0) 

If the type the client uses does not derive from IComparable, this will result in a runtime exception. In addition, when the key type used is a value type instead of the key type parameter, you force a boxing of the key, and that may have some performance implications.

Derivation Constraint

In C# 2.0, you use the where reserved keyword to define a constraint. Use the where keyword on the generic type parameter, followed by a derivation colon to indicate to the compiler that the generic type parameter implements a particular interface. For example, here is the derivation constraint required to implement the Find( )method of LinkedList:

     public class LinkedList<K,T> where K : IComparable     {        T Find(K key)        {           Node<K,T> current = m_Head;           while(current.NextNode != null)           {              if(current.Key.CompareTo(key) == 0)                      {                        break;              }              else                          {                         current = current.NextNode;              }           }           return current.Item;        }        //Rest of the implementation     } 

Note that even though the constraint allows you to use IComparable, it does not eliminate the boxing penalty when the key used is a value type, such as an integer. To overcome this, the System namespace defines the generic interface IComparable<T>:

     public interface IComparable<T>     {        int CompareTo(T other);     } 

You can constrain the key type parameter to support IComparable<T> with the key's type as the type parameter, and by doing so you not only gain type safety but also eliminate the boxing of value types when used as keys:

     public class LinkedList<K,T> where K : IComparable<K>     {...} 

In fact, all the types that supported IComparable in .NET 1.1 support IComparable<T> in .NET 2.0. This enables you to use common types for keys, such as int, string, Guid, DateTime, and so on. While IComparabl<T> is designed for sorting and ordering, .NET also defines the IEquatable<T> that you can use just for comparing:

     public interface IEquatable<T>     {       bool Equals(T other);     } 

In C# 2.0, all constraints must appear after the actual derivation list of the generic class. For example, if LinkedList derives from the IEnumerable<T> interface (for iterator support), you would put the where keyword immediately after it:

     public class LinkedList<K,T> : IEnumerable<T> where K : IComparable<K>     {...} 

When the client declares a variable of type LinkedList, providing a concrete type for the list's key, the client-side compiler will insist that the key type is derived from IComparable<T> (with the key's type as the type parameter) and will refuse to build the client code otherwise.

You can constrain multiple interfaces on the same generic type parameter, separated by a comma. For example:

     public class LinkedList<K,T> where K : IComparable<K>,IConvertible     {...} 

You can provide constraints for every generic type parameter your class uses:

     public class LinkedList<K,T> where K : IComparable<K>                                  where T : ICloneable     {...} 

You can also have a base-class constraint, stipulating that the generic type parameter is derived from a particular base class:

     public class MyBaseClass     {...}     public class LinkedList<K,T> where K : MyBaseClass     {...} 

However, you can only use at most one base class in a constraint, because C# does not support multiple inheritance of implementation. Obviously, the base class you constrain cannot be a sealed class, and the compiler enforces that. In addition, you cannot constrain System.Delegate or System.Array as a base class.

You can constrain both a base class and one or more interfaces, but the base class must appear first in the derivation constraint list:

     public class LinkedList<K,T> where K : MyBaseClass, IComparable<K>     {...} 

C# does allow you to specify a naked generic type parameter as a constraint:

     public class LinkedList<T,U> where T : U     {...} 

You can also use another generic type as a constraint:

     public interface ISomeInterface<T>     {...}     public class LinkedList<K,T> where K : ISomeInterface<int>     {...} 

When constraining another generic type as a base type, you can keep that type generic by specifying the generic type parameters of your own type parameter. For example, in the case of a generic base-class constraint:

     public class MySubClass<T> where T : MyBaseClass<T>     {...} 

Finally, note that when you provide a derivation constraint, the base type (interface or base class) you constrain must have consistent visibility with that of the generic type parameter you define. For instance, the following constraint is valid, because internal types can use public types:

     public class MyBaseClass     {}     internal class MySubClass<T> where T : MyBaseClass     {} 

However, if the visibility of the two classes were reversed, such as:

     internal class MyBaseClass     {}     public class MySubClass<T> where T : MyBaseClass     {} 

the compiler would issue an errorno client from outside the assembly will ever be able to use the generic type MySubClass, rendering MySubClass in effect an internal rather than a public type. (Outside clients cannot use MySubClass because to declare a variable of type MySubClass, they need to make use of a type that derives from the internal type MyBaseClass).

Constructor Constraint

Suppose you want to instantiate a new generic object inside a generic class. The problem is that the C# compiler does not know whether the specific type the client will use has a matching constructor, so it will refuse to compile the instantiation line. To address this problem, C# allows you to constrain a generic type parameter such that it must support a public default constructor. This is done using the new( ) constraint. For example, here is a different way of implementing the default constructor of the generic Node<K,T> from Example D-3:

     class Node<K,T> where K : new( )                     where T : new( )     {        public K Key;        public T Item;        public Node<K,T> NextNode;        public Node( )        {           Key      = new K( );           Item     = new T( );           NextNode = null;        }        //Rest of the implementation        } 

You can combine the constructor constraint with a derivation constraint, provided the constructor constraint appears last in the constraint list:

     public class LinkedList<K,T> where K : IComparable<K>,new( )                                  where T : new( )     {...} 

Class/Struct Type Constraint

You can constrain a generic type parameter to be a value type (such as an int, a bool, an enum, or any custom structure using the struct constraint):

     public class MyClass<T> where T : struct     {...} 

Similarly, you can constrain a generic type parameter to be a reference type (a class) using the class constraint:

     public class MyClass<T> where T : class     {...} 

The class/struct constraint cannot be used with base class constraint, because a base class constraint implies a class. Similarly, you cannot use the struct and the default constructor constraint, because default constructor constraint too implies a class. Though you can use the class and the default constructor constraint, it adds no value. You can combine the class/struct constraint with an interface constraint, as long as the class/struct type constraint appears first in the constraint list.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net