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.

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

Generic methods

Introduction

Whether that it is defined in a generic type or not, that it is static or not, a method has the possibility of defining it own parameter types. Each time such a method is invoked, a type must be provided for each parameter type. Here we talk about the concept of generic method.

The parameter types specific to a method can only be used within the scope of the method (i.e. the return value + signature + body of the method). In the C2<T> class of the following example, there is no correlation between the U parameter type of the Fct<U>() method and the U parameter type of the FctStatic<U>() method.

The parameter types of a method can have the same name as a parameter type for the class defining the method. In this case, the parameter type of the class is hidden within the scope of the method. In the C3<T>.Fct<T>() method of the following example, the T parameter type defined by the methods hides that T parameter type defined by the class. This practice can lead to confusion and the compiler will emit a warning when it is detected.

Example 28

class C1 {
   public U Fct<U>(U u) { return u; }
}
class C2<T> {
   public U Fct<U>(U u) { return u; }
   public static U FctStatic<U>(U u) { return u; }
}
class C3<T> {
   // Compilation warning : Type parameter ' T' has same
   // name as type parameter from outer type 'C3<T>'.
   public T Fct<T>(T t) { return t; }
}
class Program {
   static void Main() {
      C1 c1 = new C1();
      c1.Fct<double>(3.4);
      C2<int> c2 = new C2<int>();
      c2.Fct<double>(3.4);
      c2.Fct<string>("hello");
      C3<int> c3 = new C3<int>();
      c3.Fct<double>(3.4);
   }
}

This feature cannot be used on operators, on extern methods nor on the property, indexer and event accessors.

Generic methods and constraints

A generic method can define all sorts of constrains for each of its parameter types. The syntax for this is identical to the one for the definition of constraints on generic types.

Example 29

class C {
   public int Fct<U>(U u) where U : class,
      System.IComparable<U> ,new(){
      if ( u == null ) 
         return 0;
      U unew = new U();
      return u.CompareTo( unew );
   }
}

Of course, a generic method cannot override the constraints on a parameter type defined on its class.

Virtual generic methods

Abstract, virtual and interface methods can also be generic. In this case, the overloading of such methods does not need to respect the name of the parameter types. When overriding a virtual or abstract generic method which has constraints on its parameter types, you must not rewrite this set of constraints. When implementing an interface method which has constraints on its parameter types, you must rewrite this set of constraints. These rules are illustrated by the following example which compiles without warning or errors:

Example 30

using System;
abstract class B {
   public virtual A Fct1<A, C>( A a, C c ) { return a; }
   public abstract int Fct2<U>(U u) where U:class,IComparable<U>,new();
}
class D1 : B {
   public override X Fct1<X, Y>( X x, Y y ) { return x; }
   public override int Fct2<U>( U u )  { return 0; }
}
interface I {
   A Fct1<A, C>( A a, C b );
   int Fct2<U>( U u ) where U : class, IComparable<U>, new();
}
class D2 : I {
   public X Fct1<X, Y>( X x, Y y ) { return x; }
   public int Fct2<U>( U u ) where U : class, IComparable<U>, new() 
   { return 0; }
}

Inference of generic method parameter types

When a generic method is invoked, the C#2 compiler has the possibility of inferring the parameter types based on the types of the provided arguments to the method. Explicitly providing the parameter types for a generic method overrides the inference rules.

The inference rule does not take into account the type of the return value. However, the compiler is capable of inferring a parameter type from the elements of an array. This is illustrated by the following program:

Example 31

class C {
    public static U Fct1<U>() { return default(U); }
    public static void Fct2<U>( U u ) { return; }
    public static U Fct3<U>( U u ) { return default(U); }
    public static void Fct4<U>( U u1, U u2 ) { return; }
    public static void Fct5<U>( U[] arrayOfU ) { return; }
}
class Program {
   static void Main() {
      // Compilation error: The type arguments for method 
      // 'C.Fct1<U>()' cannot be inferred from the usage.
      string s = C.Fct1();

      // Compilation error: Cannot implicitly convert type 
      // 'System.IDisposable' to 'string'.
      string s = C.Fct1<System.IDisposable>();
      s = C.Fct1<string>(); // OK

      C.Fct2( "hello" ); // Infer 'U' as 'string'.

      // Compilation error: The type arguments for 
      // method 'C.Fct2<U>(U)' cannot be inferred from the usage.
      C.Fct2( null );

      int i = C.Fct3( 6 ); // Infer 'U' as 'int.

      double d = C.Fct3( 6 ); // Awkward: Infer 'U' as 'int' ...
                              // ... and not as 'double'.

      // Compilation error: Cannot implicitly convert 'int' 
      // to 'System.IDisposable'.
      System.IDisposable dispose = C.Fct3( 6 );

      // Infer 'U' as 'string'.
      C.Fct4( "hello", "bonjour" ); 

      // Compilation error: The type arguments for method 
      // 'C.Fct4<U>(U,U)' cannot be inferred from the usage.
      C.Fct4( 5, "bonjour" );

      C.Fct5( new int[6] ); // Infer 'U' as 'int.
   }
}

C#2 grammar ambiguity

There is an ambiguity in the C#2 grammar as the lesser than '<' and greater than '>' characters can be used, in certain special cases, both for the definition of the list of parameter types and as well as comparison operators. This special case is illustrated in the following example:

Example 32

class C<U,V> {
    public static void Fct1() {
        int U = 6;
        int V = 7;
        int Fct2 = 9;
        Fct3( Fct2 < U, V > (20) );    // Call Fct3(int)
        Fct3( Fct2 < U, V > 20 );      // Call Fct3(bool,bool)
    }
    public static int Fct2<A, B>( int i ) { return 0;}
    public static void Fct3( int i ) { return; }
    public static void Fct3( bool b1, bool b2 ) { return; }
}

The rule is that when the compiler is faced with this ambiguity, it analyzes the character located right after '>'. If this character is in the following list, then the compiler will infer a list of parameter types:

( ) ] > : ; , . ?
[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

  • Today's agile organizations pose operations teams with a tremendous challenge: to deploy new releases to production immediately after development and testing is completed. To ensure that applications are deployed successfully, an automatic and transparent process is required. We refer to this process as Zero Touch Deployment™. This white paper reviews two approaches to Zero Touch Deployment--a script-based solution and a release automation platform. The article discusses how each can solve the key …

  • Learn How A Global Entertainment Company Saw a 448% ROI Every business today uses software to manage systems, deliver products, and empower employees to do their jobs. But software inevitably breaks, and when it does, businesses lose money -- in the form of dissatisfied customers, missed SLAs or lost productivity. PagerDuty, an operations performance platform, solves this problem by helping operations engineers and developers more effectively manage and resolve incidents across a company's global operations. …

Most Popular Programming Stories

More for Developers

RSS Feeds