C# Generics Part 3/4: Casting, Inheritance, and Generic Methods

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

Casting and generics

Basic rules

From now on, we will suppose that T is a parameter type. The C#2 language allows you to:

  • Cast implicitly an instance of a type T (if T is a value type, else a reference of type Tobjet type. If T is a value type, a boxing operation will occur.
  • Cast explicitly a reference of type objet to an instance of type T. If T is a value type, there will be an unboxing operation.
  • Cast explicitly an instance of a type T to a reference of any interface. If T is a value type, a boxing operation will occur.
  • Cast explicitly a reference of any interface to an instance of the T type. If T is a value type, there will be an unboxing operation.
  • In the last three cases, if the cast is not possible, an exception of type InvalidCastException is raised.

Other casting rules are added if we use derivation constraints:

  • If T is constrained to implement the interface I, you can implicitly cast an instance from T into I or into any interface implemented by I and vice versa. If T is a value type, a boxing operation (or unboxing) will occur.
  • If T is constrained to derive from the C class, you can implicitly cast an instance of T to C or into any sub-class of C and vice versa. If a custom implicit conversion exist from C to a type A then the compiler will accept an implicit conversion from T to A. If a custom explicit conversion exists from A to C then the compiler will accept an explicit conversion from A to T.

Casting and generic arrays

If T is a parameter type of a generic class and if T is constrained to derive from C then the C#2 compiler will accept to:

  • Cast implicitly an array of T into an array of C. In other words, the C#2 compiler accepts to cast implicitly a reference of type T[] into a reference of C[]. We say that the C# arrays accept covariance on their elements.
  • Cast explicitly an array of C into an array of T. In other words, the C#2 compiler accepts to explicitly cast a reference of type C[] to a reference of type T[]. We say that the C# arrays accept contravariance on their elements.

These two rules are illustrated by the following example:

Example 24

class C { }
class GenericClass where T : C {
   T[] arrOfT = new T[10];
   public void Fct(){
      C[] arrOfC = arrOfT;
      T[] arrOfT2 = (T[]) arrOfC;
   }
}

There is no equivalent rule if T is constrained to implement the I interface. Also, the covariance and the contravariance are not supported on the parameter types of a generic class. In other words, if class D derives from class B, there exists no implicit conversion between a reference of type List<D> and a reference of type List<B>.

is and as operators

To avoid an exception of type InvalidCastException when you are not certain of a type conversion implicating a T type parameter, it is recommended to use the is operator to test if the conversion is possible and the as operator to attempt the conversion. Remember that the as operator returns null if the conversion is not possible. For example:

Example 25

using System.Collections.Generic;
class C<T> {
   public void Fct(T t){
      int i = t as int;    // Compilation error:
                           // The as operator must be used with a
                           // reference type.
      string s = t as string;
      if( s!= null ) { /*...*/ }
      if( t is IEnumerable<int> ){
         IEnumerable<int> enumerable = t as IEnumerable<int>;
         foreach( int j in enumerable) { /*...*/ }
      }
   }
}

Inheritance and generics

Basic rules

A non-generic class can inherit from a generic class. In this case, all parameter types must be resolved:

class B<T> {...}
class D : B<double> {...}

A generic class can derive from a generic class. In this case, it is optional to resolve all the parameters. However, it is necessary to repeat the constraints on the non-resolved parameter types. For example:

class B<T> where T : struct { }
class D1<T> : B<T> where T : struct { }
class D2<T> : B<int> { }     // Awkward: 'T' is a different
                             // parameter type.
class D3<U,V> : B<int> { }

Finally, know that a generic class can inherit from a non-generic class.

Overriding virtual methods of generic types

A generic base class can have abstract or virtual methods which uses or not parameter types in their signatures. In this case, the compiler forces the methods to be rewritten in derived classes to use the proper parameters. For example:

Example 26

abstract class  B<T> {
   public abstract T Fct(T t);
}
class D1 : B<string>{
   public override string Fct( string t ) { return "hello"; }
}
class D2<T> : B<T>{
   public override T Fct(T t) { return default (T); }
}
// Compilation error :
// Does not implement inherited abstract member 'B<U>.Fct(U)'
class D3<T, U> : B<U> {
   // Compilation error: No suitable method found to override
   public override T Fct(T t) { return default(T); }
}

We take advantage of this example to underline the fact that a generic class can also be abstract. This example also shows the type of compiler error that we will encounter when we do not properly use the parameter types.

It is interesting to note that the parameter types of a derived generic class can be used in the body of an overloaded virtual method, even if the base class is not generic.

Example 27

class B  {
   public virtual void Fct() { }
}
class D : B where T : new(){
   public override void Fct() {
      T t = new T();
   }
}

All the rules mentioned in the current section remain valid in the implementation of generic interfaces, classes or structures.

More by Author

Must Read