C# Generics, Part 2/4: Constraints, Members, Operators

The following article is excerpted from the book Practical .NET2 and C#2. The first part of this article is located here.

Type parameter constraints

C#2 allows you to impose constraints on the parameter type of a generic type. Without this feature, the generics in C#2 would be hard to exploit. In fact, it is hard to do almost anything on a parameter type of which we know nothing. We do not even know if it can be instantiated (as it can take the form of an interface or abstract case). In addition, we cannot call a specific method on an instance of such a type, we cannot compare the instances of such a type…

To be able to use a parameter type within a generic type, you can impose one or several constraints amongst the following:

  • The constraint of having a default constructor.
  • The constraint of implementing a certain interface or (non-exclusive) of deriving from a certain type.
  • The constraint of being a value type or (exclusive) being a reference type.

Note for C++ coders: The template mechanism of C++ has no need for constraints to use type parameters since these types are resolved during compilation. In this case, all attempts to use a missing member will be detected by the compiler.

Default constructor constraint

If you need to be able to instantiate an object of the parameter type within a generic, you do not have a choice but impose the default constructor constraint. Here is an example which illustrates this syntax:

Example 6

class Factory<U> where U : new() {
   public static U GetNew() { return new U(); }
}
class Program {
   static void Main(){
      int i = Factory<int>.GetNew();
      object obj = Factory<object>.GetNew();
      // Here, 'i' is equal to 0 and 'obj' references
      // an instance of the class 'object'.
   }
}

Derivation constraint

If you wish to use certain members of the instances of a parameter type in a generic, you must apply a derivation constraint. Here is an example which illustrates the syntax:

Example 7

interface ICustomInterface { int Fct(); }
class C<U> where U : ICustomInterface {
   public int AnotherFct(U u) { return u.Fct(); }
}

You can apply several interface implementation constraints and one base class inheritance constraint on a same type parameter. In this case, the base class must appear in the list of types. You can also use this constraint conjointly with the default constructor constraint. In this case, the default constructor constraint must appear last:

Example 8

interface ICustomInterface1 { int Fct1(); }
interface ICustomInterface2 { string Fct2(); }
class BaseClass{}
class C<U>
   where U : BaseClass, ICustomInterface1, ICustomInterface2, new() {
   public string Fct(U u) { return u.Fct2(); }
}

You cannot use a sealed class or a one of the System.Object, System.Array, System.Delegate, System.Enum or System.ValueType class as the base class of a type parameter.

You also cannot use the static members of T like this:

Example 9

class BaseClass { public static void Fct(){} }
class C<T> where T : BaseClass {
   void F(){
      // Compilation Error: 'T' is a 'type parameter',
      // which is not valid in the given context.
      T.Fct();
      // Here is the right syntax to call Fct().
      BaseClass.Fct();
   }
}

A type used in a derivation constraint can be an open or closed generic type. Let’s illustrate this using the System.IComparable<T> interface. Remember that the types which implement this interface can see their instances compared to an instance of type T.

Example 10

using System;
class C1<U> where U : IComparable<int> {
   public int Compare( U u, int i ) { return u.CompareTo( i ); }
}
class C2<U> where U : IComparable<U> {
   public int Compare( U u1, U u2 ) { return u1.CompareTo( u2 ); }
}
class C3<U,V> where U : IComparable<V> {
   public int Compare( U u, V v ) { return u.CompareTo( v ); }
}
class C4<U,V> where U : IComparable<V>, IComparable<int> {
   public int Compare( U u, int i ) { return u.CompareTo( i ); }
}

Note that a type used in a derivation constraint must have a visibility greater or equal to the one of the generic type which contains this parameter type. For example:

Example 11

internal class BaseClass{}
// Compilation Error: Inconsistent accessibility:
// constraint type 'BaseClass' is less accessible than 'C<T>'
public class C<T> where T : BaseClass{}

To be used in a generic type, certain functionalities can force you to impose certain derivation constraints. For example, if you wish to use a T type parameter in a catch clause, you must constrain T to derive from System.Exception or of one of its derived classes. Also, if you wish to use the using keyword to automatically dispose of an instance of the type parameter, it must be constraint to use the System.IDisposable interface. Finally, if you wish to use the foreach keyword to enumerate the elements of an instance of the parameter type, it must be constraint to implement the System.Collections.IEnumerable or System.Collections.Generic.IEnumerable<T> interface.

Take note that in the special case where T is constrained to implement an interface and T is a value type, the call to a member of the interface on an instance of T will not cause a boxing operation. The following example puts this into evidence:

Example 12

interface ICounter{
   void Increment();
   int Val{get;}
}
struct Counter : ICounter {
   private int i;
   public void Increment() { i++; }
   public int Val { get { return i; } }
}
class C<T> where T : ICounter, new() {
   public void Fct(){
      T t = new T();
      System.Console.WriteLine( t.Val.ToString() );
      t.Increment();    // Modify the state of 't'.
      System.Console.WriteLine( t.Val.ToString() );

      // Modify the state of a boxed copy of 't'.
      (t as ICounter).Increment();
      System.Console.WriteLine( t.Val.ToString() );
   }
}
class Program {
   static void Main() {
      C<Counter> c = new C<Counter>();
      c.Fct();
   }
}

This program displays:

0
1
1

More by Author

Must Read