C# Generics Part 4/4: Delegates, Events, Reflection, Attributes, IL, Generics in the BCL

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

Delegates, events and generics

Introduction

As with all nested types, a delegate class can use the parameter types of the nesting class:

Example 33

class C<T> {
    public delegate T GenericDelegate( T t );
    public static T Fct( T t ) { return t; }
}
class Program {
   static void Main() {
       C<string>.GenericDelegate genericDelegate = C<string>.Fct;
       string s = genericDelegate( "hello" );
   }
}

A delegate class can also define its own parameter types as well as their constraints:

Example 34

public delegate U GenericDelegate<U>( U u ) where U : class;
class C<T> {
   public static T Fct( T t ) { return t; }
}
class Program {
   static void Main() {
      GenericDelegate<string> genericDelegate = C<string>.Fct;
      string s = genericDelegate( "hello" );
   }
}

Generic delegates and generic methods

When assigning a generic method to a generic delegate object, the C#2 compiler is capable of inferring the parameter types of the generic method from the parameter types of the generic delegate object. This is illustrated by the following example:

Example 35

delegate void GenericDelegateA<U>(U u);
delegate void GenericDelegateB(int i);
delegate U    GenericDelegateC<U>();
class Program {
   static void Fct1<T>(T t) { return; }
   static T    Fct2<T>() { return default(T); }
   static void Main() {
      GenericDelegateA<string> d1 = Fct1; // The compiler infers
                                          // 'Fct1<string>'.
      GenericDelegateB d2 = Fct1; // The compiler infers 'Fct1<int>'.
      GenericDelegateC<string> d3 = Fct2<string>; // OK but no
                                                  // inference.

      // Compilation error: The type arguments for method
      // 'Program.Fct2<T>()' cannot be inferred from the usage.
      GenericDelegateC<string> d4 = Fct2;
   }
}

As illustrated in this example, there is never any inference on the parameter types of a generic delegate object.

Contravariance, covariance, delegates and generics

Here, we will expose a new functionality of delegate objects which will come in handy later. In C#2, delegate objects support the notion of contravariance on their arguments and the notion of covariance on their return type. This is illustrated below:

Example 36

class Base { }
class Derived : Base { }
delegate Base DelegateType ( Derived d );
class Program{
   static Derived Handler ( Base b ) { return b as Derived; }
   static void Main() {
      // Notice that the 'Handler()' method signature is not ...
      // ... the same as the 'DelegateType' signature.
      DelegateType delegateInstance = Handler;
      Base b = delegateInstance( new Derived() );
   }
}

It is legitimate for this program to compile properly. Think about it in terms of contract:

  • The Handler(Base) method has a contract less strict on its input than the one proposed by the DelegateType(Derived) delegate class. An instance of Handler() method without risking an invalid downcast. This is contravariance.
  • The Derived Handler() method has a more strict contract on its output than the one proposed by the Base DelegateType() delegate class. There also, an instance of DelegateType can reference the Handler() method without risking an invalid downcast. This is covariance.

Now, lets assume that the notions of covariance and of contravariance did not exist and that we wished to called the Derived Handler(Base) method through a delegate object of type Base delegate(Derived). We could in fact use a generic delegate object like this:

Example 37

class Base { }
class Derived : Base { }
delegate B DelegateType<B,D>(D d);
class Program {
   static Derived Handler(Base b){return b as Derived;}
   static void Main() {
      DelegateType<Base, Derived> delegateInstance = Handler;
      // The in reference is implitly casted from 'Derived' to 'Base'.
      // The out reference is implitly casted from 'Derived' to 'Base'.
      Base b = delegateInstance( new Derived() );
   }
}

Events and generic delegates

Generic delegates can be used to avoid the definition of multiple delegates in order to type events. The following example shows that with a single generic delegate class, you can type all events which take in a single ‘sender‘ parameter and one argument:

Example 38

delegate void GenericEventHandler<U,V>( U sender, V arg );
class Publisher {
   public event GenericEventHandler<Publisher,System.EventArgs> Event;
   public void TriggerEvent() { Event(this, System.EventArgs.Empty); }
}
class Subscriber {
   public void EventHandler( Publisher sender, System.EventArgs arg ){}
}
class Program {
   static void Main() {
      Publisher publisher = new Publisher();
      Subscriber subscriber = new Subscriber();
      publisher.Event += subscriber.EventHandler;
   }
}

Reflection, attributes, IL and generics

Generics and the System.Type class

Remember that an instance of System.Type can be obtained either using the typeof operator or by calling the object.GetType() method. With .NET 2, an instance of System.Type can reference an opened or closed generic type.

Example 39

using System;
using System.Collections.Generic;
class Program {
    static void Main() {
        List<int> list = new List<int>();
        Type type1 = list.GetType();
        Type type2 = typeof( List<int> );
        Type type3 = typeof( List<double> );
        // type4 is an open generic type.
        Type type4 = type3.GetGenericTypeDefinition();
        System.Diagnostics.Debug.Assert( type1 == type2 );
        System.Diagnostics.Debug.Assert( type1 != type3 );
        System.Diagnostics.Debug.Assert( type3 != type4 );
    }
}

The System.Type class supports new methods and properties dedicated to generics:

public abstract class Type : System.Reflection.MemberInfo,
                             System.Runtime.InteropServices._Type,
                             System.Reflection.IReflect
{
   // Returns from a generic type a new generic Type instance based
   // on the provided parameter types.
   public virtual System.Type MakeGenericType(
                                params System.Type[] typeArgs);

   // Returns an array of Type objects which represent the parameter
   // types of a generic type.
   public virtual System.Type[] GetGenericArguments();

   // Returns an open generic type from a generic type.
   public virtual System.Type GetGenericTypeDefinition();

   // Returns true if it's an open generic type.
   public virtual bool IsGenericTypeDefinition { get; }

   // Returns true if some parameter types are not specified.
   public virtual bool ContainsGenericParameters { get; }

   // Returns true if the current type is a parameter type not
   // specified for a generic type or method.
   public virtual bool IsGenericParameter { get; }

   //---------------------------------------------------------------
   // Following members can only be called on Type instances for
   // which the 'IsGenericParameter' property returns true.

   // Returns the 0-based position in the list of parameter types.
   public virtual int GenericParameterPosition { get; }

   // Returns the generic method which declares the parameter type.
   // Returns null if not declared in a generic method.
   public virtual System.Reflection.MethodBase DeclaringMethod { get; }

   // Returns derivation constraints on the current parameter type.
   public virtual System.Type[] GetGenericParameterConstraints();

   // Returns constraints other than the derivation ones.
   public virtual System.GenericParameterAttributes
              GenericParameterAttributes { get; }
   ...
}

Here is an example of the use of this class used to locate the definition of a generic type:

Example 40

using System;
using System.Reflection;
class Program {
   static void WriteTypeConstraints(Type type ){
      string[] results = new string[type.GetGenericArguments().Length];
      // For each parameter types.
      foreach (Type t in type.GetGenericArguments()) {
         // If 't' is a parameter type not specified?
         if ( t.IsGenericParameter ) {
            int pos = t.GenericParameterPosition;
            Type[] derivConstraints = t.GetGenericParameterConstraints();
            MethodBase methodBase = t.DeclaringMethod;
            GenericParameterAttributes attributes =
                        t.GenericParameterAttributes;
            results[pos] = "   where " + t.Name + ":";
            if ((GenericParameterAttributes.ReferenceTypeConstraint &
                 attributes) != 0 ) {
               results[pos] += "class,";
            }
            if((GenericParameterAttributes.
                NotNullableValueTypeConstraint & attributes) != 0 ) {
               results[pos] += "struct,";
            }
            foreach (Type derivConstraint in derivConstraints) {
               results[pos] += derivConstraint.Name + ",";
            }
            if ((GenericParameterAttributes.
                 DefaultConstructorConstraint & attributes) != 0 ) {
               results[pos] += "new()";
            }
         }    // end -- If 't' is a parameter type not specified?
      }       // end -- For each parameter types.
      Console.WriteLine(type.Name);
      foreach (string result in results)
         if (result != null)
            Console.WriteLine(result);
   }

   class Bar{}
   class Foo : Bar, IDisposable{ public void Dispose() {} }
   class C&lt;U, V&gt;
       where U : Bar, IDisposable, new()
       where V : struct {}

   static void Main() {
      WriteTypeConstraints( typeof(C&lt;Foo,int&gt;) );
      WriteTypeConstraints( typeof(C&lt;Foo,int&gt;).
         GetGenericTypeDefinition());
   }
}

This program displays:

C`2
C`2
where U:Bar,IDisposable,new()
where V:struct,ValueType,new()

We can see that the constraint forcing the type to be a value type actually adds the constraints of deriving from ValueType and of implementing a default constructor.

More by Author

Must Read