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

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

Reference\Value type constraint

The value/reference type constraint allows constraining a parameter type to be either a value type or a reference type. This constrain must be used first in the list of constraints for a given parameter type, using the struct keyword to constrain to a value type and class keyword to constrain to a reference type. Caution, this syntax can lead to confusion since classes represent a subset of referenced types (as they also include interfaces) and structures represent a subset of value types. This constraint can be useful in certain cases where we wish to test a reference against null (as an instance of a value type cannot be null) or when we want to ensure that a parameter type used with the lock keyword is a reference type.

Example 13

class C<U> where U : class, new () {
   U u = new U();
   void Fct(){ lock(u){ } }
}

Members of generic types

Method overloading

Properties, constructors, methods and indexers can be overloaded in a generic class. However, there can be ambiguity when a certain combination of parameter types causes several of the overloads to have the same signature. In this case, the preference will go towards the overload with the signature which contains the least amount of parameter types. If such a method cannot be found, the compiler will emit an error letting you know about the ambiguous call. Here is an example which should clarify these rules:

Example 14

interface I1<T> {}
interface I2<T> {}
class C1<U> {
   public void Fct1(U u){}      // This method can't ...
   public void Fct1(int i){}    // ... be called if 'U' is 'int'.

   public void Fct2(U u1, U u2){}    // Not ambiguous.
   public void Fct2(int i, string s){}

   public void Fct3(I1<U> a){}    // Not ambiguous.
   public void Fct3(I2<U> a){}

   public void Fct4(U a){}    // Not ambiguous.
   public void Fct4(U[] a){}
}
class C2<U,V> {
   public void Fct5(U u, V v){}    // Might be ambiguous if 'U'='V'.
   public void Fct5(V v, U u){}

   public void Fct6(U u, V v){}    // Might be ambiguous if ...
   public void Fct6(V v, U u){}    // ... 'U'='V' and 'V'!='int'.
   public void Fct6(int u, V v){}

   public void Fct7(int u, V v){}    // Might be ambiguous if ...
   public void Fct7(U u, int v){}    // ... 'U'='V'='int'.

   public void Fct8(U u, I1<V> v){}    // Might be ambiguous ...
   public void Fct8(I1<V> v, U u){}    // ... for example c2<I1<int>,int>.

   public void Fct9(U u1, I1<V> v2){}  // Not ambiguous.
   public void Fct9(V v1, U u2){}

   public void Fct10(ref U u){}    // Not ambiguous.
   public void Fct10(out V v){ v = default(V); }
}
class Program {
   static void Main(){
      C1<int> a = new C1<int>();
      a.Fct1(34);        // Call 'Fct1(int i)'.
      C2<int, int> b = new C2<int, int>();
      b.Fct5(13, 14);    // Compilation Error: This call is ambiguous.
      b.Fct6(13, 14);    // Call 'Fct6(int u, V v)'.
      b.Fct7(13, 14);    // Compilation Error: This call is ambiguous.
      C2<I1<int>,int> c = new C2<I1<int>,int>();
      c.Fct8(null,null); // Compilation Error: This call is ambiguous.
   }
}

Static fields

When a generic type contains a static field, there are as many versions during execution as there are closed generic types constructed from this generic type. This rule applies independently of the fact that the type of the static field is dependant or not on the parameter type. This rule is also applied independently of the fact that the parameter types of this generic are value or reference types. This last remark is relevant since closed generics which take reference types as parameter types all share the same implementation. All this is illustrated by the following example:

Example 15

using System;
class C<T> {
   private static int m_NInst = 0;
   public C() { m_NInst++; }
   public int NInst { get { return m_NInst; } }
}
class Program {
   static void Main() {
      C<int>    c1 = new C<int>();
      C<int>    c2 = new C<int>();
      C<int>    c3 = new C<int>();
      C<string> c4 = new C<string>();
      C<string> c5 = new C<string>();
      C<object> c6 = new C<object>();
      Console.WriteLine( "NInst C<int>   : " + c1.NInst.ToString() );
      Console.WriteLine( "NInst C<string>: " + c4.NInst.ToString() );
      Console.WriteLine( "NInst C<object>: " + c6.NInst.ToString() );
   }
}

This program displays:

NInst C<int> : 3
NInst C<string>: 2
NInst C<object>: 1

Static methods

A generic type can have static methods. In this case it is necessary to resolve the parameter types when such a method is invoked. For example:

Example 16

class C<T> {
   private static T t;
   public static void ChangeState(T _t){ t = _t; }
}
class Program {
   static void Main() {
      C<int>.ChangeState(5);
   }
}

The Main() static method, which is the entry point to a program, cannot be in a generic class.

Class constructor

If a generic type contains a class constructor, it is called by the CLR when it creates each of the closed generic types. We can take advantage of this feature to add our own constraints on the parameter types. For example, we cannot strictly constrain a parameter type to not be the int type. We can take advantage of the class constructor to verify such a constraint as follows:

Example 17

using System;
class C<T> {
   static C() {
      int a=0;
      if( ( (object) default(T) != null ) && a is T )
      throw new ArgumentException("Don't use the type C<int>.");
   }
}

Note the null test on the default value for T. In fact, the (a is T) is true when T is of type object and when T is of type int. To eliminate the first case, we count on the fact that (object)defaut(object) will return null.

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

Operator overloading

Although that it can lead to hard to read code, a generic type allows the overloading of operators. There is nothing particular to mention in regards to the arithmetic and comparison operation.

However, when we define a cast operator of a Src source type to a Dest destination type, the compiler must not find an inheritance relationship between the two types when the generic type is compiled. For example:

Example 18

class C<T>{}
class D<T> : C<T>{
   public static implicit operator C<int>(D<T> val) {}   // OK
   // Compiler error: 'D<T>.implicit operator C<T>(D<T>)':
   // user-defined conversion to/from base class.
   public static implicit operator C<T>(D<T> val) {}
}
class Program{
   static void Main() {
      D<int> dd = new D<int>();    // OK
   }
}

A consequence of the fact that we can redefine certain casting operators in a generic type makes it possible to redefine certain predefined type conversions. In the following example, if the parameter type U is the objet type we can redefine the implicit conversion operator of D<object> to object:

class D<U> {
   public static implicit operator U(D<U> val) { return default(U); }
}

In this case, two rules are applied by the CLR:

  • If a predefined implicit conversion exists from the Src type to the Dest type, then any redefinition (implicit or explicit) of this conversion is ignored.

  • If a predefined explicit conversion exists from the Src type to the Dest type, then all redefinitions of this explicit conversion are ignored. However, an eventual implicit redefinition of the Src to Dest conversion is used.

Nested types

A nested type within a generic type is implicitly a generic type itself. The parameter types of the nesting generic type can be freely used within the nested type. A nested type within a generic type has the possibility of having its own parameter types. In this case, there will be a closed generic type generated by the CLR for each different combination of parameter types used.

Example 19

using System;
class Outer<U> {
   static Outer(){ Console.WriteLine("Hello from Outer .cctor."); }
   public class Inner<V> {
      static Inner(){ Console.WriteLine("Hello from Inner .cctor."); }
   }
}
class Program {
   static void Main() {
      Outer<string>.Inner<int> a = new Outer<string>.Inner<int>();
      Outer<int>.Inner<int> b = new Outer<int>.Inner<int>();
   }
}

This program displays:

Hello from Inner .cctor.
Hello from Inner .cctor.

Operators and generics

Equality, inequality and comparison operators on parameter type instances

The equality and inequality operators can not be used with an instance or a reference of a parameter type in the following cases:

  • If T has a derivation constraint of a class or T has a reference type constraint, then the equality and inequality operators can be used between a reference of type T and any other reference.

  • If T does not have a value type constraint, then the equality and inequality operators can be used between a reference of type T and the null reference. If T takes the form of a value type, the equality test will be false and the inequality test will be true.

Let's illustrate these rules through the following example:

Example 20

class C<T,U,V> where T : class where V :struct {
   public void Fct1( T t , U u , V v , object o, int i) {
      if ( t == o ) { }       // OK
      if ( u == o ) { }       // Compilation error.
      if ( v == o ) { }       // Compilation error.
      if ( v == i ) { }       // Compilation error.
      if ( u == null ) { }    // OK
      if ( v == null ) { }    // Compilation error.
   }
   public void Fct2(T t1, U u1, V v1, T t2, U u2, V v2) {
      if ( t1 == t2 ) { }     // OK
      if ( u1 == u2 ) { }     // Compilation error.
      if ( v1 == v2 ) { }     // Compilation error.
   }
}

The comparison operators <, >, <=, >= can never be used with an instance or a reference of a parameter type T.

The typeof operator and generics

The typeof operator used on a parameter type returns the instance of the Type class which corresponds to the current value of this parameter type.

The typeof operator used on a generic type returns the instance of the Type class corresponding to the set of current values for the parameter types.

This behavior is not obvious since the Name property of the returned types does not display the name of the parameter types.

Example 21

class C<T>{
   public static void PrintTypes(){
      System.Console.WriteLine( typeof( T ).Name );
      System.Console.WriteLine( typeof( C<T> ).Name );
      System.Console.WriteLine( typeof( C<C<T>> ).Name );
      if( typeof( C<T> ) != typeof( C<C<T>> ) ) {
         System.Console.WriteLine("Despite a similar name" +
                                  " they are different instances of
                                    Type.");
      }
   }
}
class Program {
   static void Main() {
      C<string>.PrintTypes();
      C<int>.PrintTypes();
}
}

This program displays:

String
C`1
C`1
Despite a similar name they are different instances of Type.
Int32
C`1
C`1 Despite a similar name they are different instances of Type.

The same is not true if we use the FullName property:

Example 22

...
   public static void PrintTypes(){
      System.Console.WriteLine( typeof( C<T> ).FullName );
      System.Console.WriteLine( typeof( C<C<T> >).FullName );
   }
...

This program displays:

C`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
C`1[[C`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]], AsmTest, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null]]
C`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
C`1[[C`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]], AsmTest, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null]]

params and lock keywords and generics

A parameter type can be used to type a params argument in the signature of a method.

A parameter type can also be used to type the object of a lock clause. This feature presents a danger when the parameter type is a value type. You must be aware that in this case, the lock keyword will have no effect. It is actually surprising that the compiler does not force a parameter type used in a lock clause to have a constraint which forces it to be a reference type.

The default operator

In the stack example, we had considered the Pop() operation on an empty stack to be an error in the use of the Stack<T> class by the client (i.e. a violation of the contract presented by a stack). We could have weakened this contract and considered this operation as a possible event. In the first case, raising an exception is the proper behavior. In the second case, it would be better to return an empty element which the client would interpret as: there are no more elements on my stack. However, we know nothing about the type T of the element to return. If T is a reference type, we wish to return a null reference while if T is a value type such as int we wish to return 0. The default operator of C#2 allows you to obtain the default value for a type.

Example 23

class Stack<T>{
   ...
   public T Pop(){
      if (m_Index == 0)
         return default(T);
      return m_ItemsArray[--m_Index];
   }
   ...
}
[cover.jpg] The preceding article is excerpted from the book Practical .NET2 and C#2 by Patrick Smacchia. Publisher: Paradoxal Press. ISBN: 0-9766132-2-0. (Browse and download the 647 listings and sample chapters.)


About the Author

Patrick Smacchia

Patrick Smacchia is a .NET MVP involved in software development for over 15 years. He is the author of Practical .NET2 and C#2 (http://www.PracticalDOT.NET), a .NET book conceived from real world experience with 647 compilable code listings. After graduating in mathematics and computer science, he has worked on software in a variety of fields including stock exchange at Societe Generale, airline ticket reservation system at Amadeus as well as a satellite base station at Alcatel. He's currently a software consultant and trainer on .NET technologies as well as the author of the freeware NDepend which provides numerous metrics and caveats on any compiled .NET (http://www.NDepend.com) application.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: May 7, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT This eSeminar will explore three popular games engines and how they empower developers to create exciting, graphically rich, and high-performance games for Android® on Intel® Architecture. Join us for a deep dive as experts describe the features, tools, and common challenges using Marmalade, App Game Kit, and Havok game engines, as well as a discussion of the pros and cons of each engine and how they fit into your development …

  • Instead of only managing projects organizations do need to manage value! "Doing the right things" and "doing things right" are the essential ingredients for successful software and systems delivery. Unfortunately, with distributed delivery spanning multiple disciplines, geographies and time zones, many organizations struggle with teams working in silos, broken lines of communication, lack of collaboration, inadequate traceability, and poor project visibility. This often results in organizations "doing the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds