Le support de la généricité par C#2 par Patrick Smacchia

 

Pré-requis. 1

Introduction. 1

Un problème de C#1 et sa résolution grâce aux types génériques de C#2. 1

Le problème du typage des éléments d’une collection en C#1. 1

Résolution élégante du problème à l’aide d’une classe générique de C#2. 2

Vue d’ensemble de la généricité de C#2. 2

Possibilité pour un type d’être génériques sur plusieurs types. 2

Types génériques ouverts et fermés. 2

La généricité de .NET vs. le mécanisme de templates de C++. 2

Visibilité d’un type générique. 3

Structures et interfaces génériques. 3

Possibilité de créer des alias sur le nom d’un type générique fermé. 3

Possibilité de contraindre un type paramètre. 3

La contrainte du constructeur par défaut 3

Contraintes de dérivation. 3

La contrainte type valeur/type référence. 4

Les contraintes qui vont manquer dans C#2. 4

Les membres d'un type générique. 4

Surcharge de méthode. 4

Les champs statiques. 4

Les méthodes statiques. 5

Le constructeur statique. 5

Surcharge des opérateurs. 5

Les types encapsulés. 5

Les opérateurs et les types génériques. 5

Utilisation des opérateurs d’égalité, d’inégalité et de comparaison avec une instance d’un type paramètre. 5

L’opérateur typeof 5

Les mots clés params et lock. 6

L’opérateur default 6

Le transtypage (casting) et la généricité. 6

Les règles de base. 6

Transtypage entre tableaux. 6

Les opérateurs is et as. 6

L’héritage et la généricité. 6

Les différents cas. 6

Surcharge de méthodes virtuelles d’un type générique. 6

Les méthodes génériques. 7

Introduction. 7

Méthodes génériques et contraintes. 7

Méthodes virtuelles génériques. 7

Inférence des types paramètres selon les types des paramètres d’une méthode générique. 7

Ambiguïté dans la grammaire de C#2. 7

Les délégués, les évènements et la généricité. 8

Introduction. 8

Délégués génériques et méthodes génériques. 8

Contravariance, covariance, délégués et généricité. 8

Evènements et délégués génériques. 8

Réflexion, attribut, IL et généricité. 8

Evolution de la classe System.Type. 8

Evolution des classes System.Reflection.MethodBase et System.Reflection.MethodInfo. 9

Les attributs et la généricité. 10

La généricité et le langage IL. 10

La généricité et le framework .NET. 10

La sérialisation et la généricité. 10

.NET Remoting et la généricité. 10

Les collections et la généricité. 10

Les domaines ne supportant pas la généricité. 11

Conclusion. 11

 

Pré-requis

Une bonne connaissance générale de C#1.

Introduction

Depuis deux ans  déjà, DNG s’intéresse de près au support de la généricité par .NET2, basé sur les travaux des chercheurs du laboratoire MS Research Don Syme et Andrew Kennedy. Rien d’étonnant à cela quand on sait que cette caractéristique est considérée par beaucoup comme l’évolution centrale de la version 2 de .NET (Bill Gates a veillé personnellement à ce que le support des génériques soit prêt pour cette version).

J’ai pu m’apercevoir lors de la rédaction du présent article que l’implémentation des génériques n’est pas encore stabilisée à 100%. Les différentes sources d’information (articles, blogs, newsgroups, ouvrage The C# programming language) sont parfois contradictoires et certains résultats obtenus par mes propres tests laissent à penser qu’il reste une poignée de cas à améliorer. En bref, tous ce qui va être dit ici n’est valable que pour la version Beta1 de .NET (build 2.0.40607) et sera rigoureusement vérifié au cas où ce contenu se transformerait en un chapitre un jour...

Dans cet article, nous allons examiner à la loupe la plupart des implications du support de la généricité, au niveau du langage C#2, du CLR et du Framework. Mais avant cela commençons par une petite introduction à la généricité.

Un problème de C#1 et sa résolution grâce aux types génériques de C#2

Le problème du typage des éléments d’une collection en C#1

Supposons que nous avons à implémenter une classe Stack (pile en français) qui permet d’empiler et de dépiler des éléments. Pour simplifier, nous considérons que la pile ne peut contenir plus qu’un certains nombre d’éléments ce qui nous permet d’utiliser en interne un tableau C#. Voici une implémentation de la classe Stack qui satisfait ces contraintes :

class Stack{

   private object[] m_ItemsArray;

   private int m_Index = 0;

   public const int MAX_SIZE = 100;

   public Stack(){ m_ItemsArray = new object[MAX_SIZE]; }

   public Object Pop(){

      if (m_Index ==0 )

         throw new InvalidOperationException("Impossible de dépiler un élément d'une pile vide.");

      return m_ItemsArray[--m_Index];

   }

   public void Push(Object item) {

      if(m_Index == MAX_SIZE)

         throw new StackOverflowException("Impossible d'empiler un élément sur une pile pleine.");  

      m_ItemsArray[m_Index++] = item;

   }

}

Cette implémentation souffre de trois défauts majeurs.

Premièrement, les clients de la classe Stack doivent transtyper explicitement tout élément obtenu à partir de la pile. Par exemple :

...

Stack stack = new Stack();

stack.Push(1234);

int number = (int)stack.Pop();

...

Un deuxième problème moins apparent se situe au niveau des performances. Il faut être conscient que lorsque l’on utilise notre classe Stack avec des éléments de type valeur, nous réalisons implicitement une opération de boxing à l’insertion d’un élément et une opération de unboxing à la récupération d’un élément. Ce phénomène est mis en évidence par la version IL du client ci-dessus :

L_0000: newobj instance void Stack::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldc.i4 1234
L_000c: box int32
L_0011: callvirt instance void Stack::Push(object)
L_0016: nop 
L_0017: ldloc.0 
L_0018: callvirt instance object Stack::Pop()
L_001d: unbox int32
L_0022: ldind.i4 
L_0023: stloc.1 
L_0024: ret 

Enfin, un troisième problème vient du fait que l’on peut empiler des éléments de types différents dans une même instance de la classe Stack. Or, en général nous souhaitons avoir des piles d’éléments qui partagent un même type. Cette possibilité peut facilement mener à des problèmes de transtypages qui ne sont découverts qu’à l’exécution comme dans l’exemple ci-dessous :

...

Stack stack = new Stack();

stack.Push("1234");

int number = (int)stack.Pop(); // Provoque une exception de type InvalidCastException

...

Lorsqu’un problème de transtypage n’est pas détecté à la compilation mais qu’il provoque une exception à l’exécution, on dit que le code n’est pas type-safe. Or, dans le développement logiciel comme dans toute discipline, plus une erreur est détectée tôt dans le processus de production moins elle est nuisible. Il faut donc dans la mesure du possible avoir du code type-safe puisque celui-ci permet la détection d’erreurs au plus tôt, lors de la compilation.

Il est possible d’implémenter notre concept de pile d’une manière type-safe. Nous pourrions en effet décider d’implémenter une classe StackOfInt pour décrire une pile contenant des entiers, une classe StackOfSring pour décrire une pile contenant des chaînes de caractères etc

class StackOfInt{

   private int[] m_ItemsArray;

   private int m_Index = 0;

   public const int MAX_SIZE = 100;

   public StackOfInt(){m_ItemsArray = new int[MAX_SIZE];}

   public int Pop() {...}

   public void Push(int item) {...}

}

class StackOfString{

   private string[] m_ItemsArray;

   private int m_Index = 0;

   public const int MAX_SIZE = 100;

   public StackOfString(){m_ItemsArray = new string[MAX_SIZE];}

   public string Pop() {...}

   public void Push(string item) {...}

}

...

Bien qu’elle soit type-safe et qu’elle résolve à la fois le problème de transtypage et le problème de performance cette solution n’est clairement pas satisfaisante. Elle implique de la duplication de code puisque la logique d’une pile est implémentée par plusieurs classes. Les conséquences sont plus de code à maintenir et donc une baisse de la productivité.

Résolution élégante du problème à l’aide d’une classe générique de C#2

C#2 permet de résoudre élégamment le problème de la section précédente grâce à l’introduction des types génériques. Concrètement, nous pouvons implémenter une liste d’éléments de type T en laissant la liberté aux clients de spécifier le type T lorsqu’ils instancient la classe. Par exemple :

class Stack<T>{

   private T[] m_ItemsArray;

   private int m_Index = 0;

   public const int MAX_SIZE = 100;

   public Stack(){ m_ItemsArray = new T[MAX_SIZE]; }

   public T Pop(){

      if (m_Index ==0 )        throw new InvalidOperationException("Impossible de dépiler un élément d'une pile vide.");

      return m_ItemsArray[--m_Index];

   }

   public void Push(T item) {

      if(m_Index == MAX_SIZE)  throw new StackOverflowException("Impossible d'empiler un élément sur une pile pleine.");  

      m_ItemsArray[m_Index++] = item;

   }

}

Et voici à quoi ressemble un client de la classe Stack<T>:

...

Stack<int> stack = new Stack<int>();

stack.Push(1234);

int number = stack.Pop(); // plus besoin de casting

stack.Push(5678);

string sNumber = stack.Pop(); // Erreur de compilation: Cannot implicitly convert type 'int' to 'string'

...

Cette solution ne souffre d’aucun des problèmes vus précédemment :

·         Le client n’a plus besoin de transtyper un élément récupéré de la pile.

·         Cette solution n’entraîne aucune opération de boxing/unboxing.

·         Le client écrit du code type-safe. Il n’a pas la possibilité d’avoir à l’exécution une pile d’éléments de types différents. Dans notre exemple, le compilateur interdit toute insertion ou récupération d’un élément d’un type différent que int ou qui n’est pas implicitement convertible en int.

·         Il n’y a aucune duplication de code.

Notez bien que dans notre exemple la classe générique est Stack<T> alors que T est le type qui paramètre notre classe générique. On dit que T est un type paramètre. On utilise parfois le terme polymorphisme paramétré (parametric polymorphism en anglais) pour désigner la généricité. En effet, notre classe Stack<T>  peut prendre plusieurs formes (Stack<int>, Stack<string>…). Elle est donc polymorphe et paramétrée par un type. Attention, il ne faut pas confondre ceci avec le polymorphisme traditionnel des langages objets qui permet de manipuler différentes formes d’objets (i.e des objets instances de classes différentes) au travers d’une même interface.

Concluons cette analyse par une citation pertinente de Rémi Forax: la classe Stack<T> représente n’importe quelle pile alors qu’une pile d’objets est une pile de n’importe quoi.

Vue d’ensemble de la généricité de C#2

Possibilité pour un type d’être génériques sur plusieurs types

Il peut être utile de paramétrer un type par plusieurs type. C#2 présente cette possibilité. Par exemple, comme le montre l’exemple ci dessous, il est possible d’implémenter une classe dictionnaire qui laisse la possibilité aux clients de choisir le type des clés et le type des valeurs :

class DictionaryEntry<K,V>{

   public K Key;

   public V Value;

}

class Dictionary<K,V>{

   private DictionaryEntry<K,V>[] m_ItemsArray;

   public void Insert(DictionaryEntry<K,V> entry) {...}

   public V Get(K key) {...}

   ...

}

Types génériques ouverts et fermés

Un type générique (parfois aussi nommé type construit) est un type paramétré par un ou plusieurs autres types. Par exemple Stack<T>, Stack<int>, Dictionary<K,V>, Dictionary<int,V>, Dictionary<int,string>,et Stack<Stack<T>> sont des types génériques.

Un type générique fermé (parfois aussi nommé type construit fermé) est un type générique pour lequel tous les types paramètres sont précisés : Par exemple Stack<int>, Dictionary<int,string>,et Stack<Stack<int>> sont des types génériques fermés.

Un type générique ouvert (parfois aussi nommé type construit ouvert) est un type générique pour lequel au moins un type paramètre n’est pas précisé : Par exemple Stack<T>, Dictionary<int,V>, Dictionary<K,V>, et Stack<Stack<T>> sont des types génériques ouverts.

Un type générique se compile en un seul type dans son assemblage. Concrètement, si l'on analyse l'assemblage qui contient le type générique ouvert Stack<T>, nie dans un certain assemblageegeeéciséstruit ctions. on s’aperçoit que la compilation n'a produit qu'une seule classe indépendamment du fait qu’un client peut par exemple utiliser les types génériques fermés Stack<int>, Stack<bool>, Stack<double>, Stack<string>, Stack<object> et Stack<IDispose>.

En revanche, à l'exécution, le CLR crée et utilise plusieurs classes Stack<T>. Plus précisément, le CLR utilise une même version de Stack<T> commune à tous les types paramètres références et une version de Stack<T> pour chaque type paramètre valeur. 

La généricité de .NET vs. le mécanisme de templates de C++

Ceux qui connaissent le C++ auront certainement rapproché les types génériques de C# aux templates de C++. Bien que ces fonctionnalités soient conceptuellement proches, la présente section exhibe une différence fondamentale :

·         Les types génériques fermés engendrés par les templates C++ sont produits par le compilateur C++ et sont contenus dans le composant produit de la compilation.

·         Les types génériques fermés engendrés par la généricité .NET sont produits à l’exécution par le compilateur JIT et le type générique sous-jacent n’est présent qu’en une seule version dans l’assemblage produit de la compilation.

Autrement dit, la notion de type générique ouvert existe en C#/.NET au niveau du code source, du composant et du runtime alors qu’en C++, elle n’existe qu’au niveau du code source.

Cette remarque souligne clairement un atout à la généricité de C# puisque la taille des composants .NET est d’autant réduite. Cela n’est pas négligeable puisque le phénomène de gonflement de la taille des composants C++, connu sous le nom de code-bloat, peut être parfois très pénalisant (sans compter l’avalanche de warning produite par certains compilateurs C++).

Il peut y avoir cependant du code-bloat en .NET dans une moindre mesure. En effet, les types génériques fermés créés à l’exécution par le CLR ne sont jamais ‘garbage-collectés’. Ils résident dans leurs domaines d’application jusqu’à ce que celui-ci soit détruit. Dans certains cas rares résolubles en déchargeant à la main un domaine d’application, il peut donc y avoir un encombrement de la mémoire. Un bon point pour la généricité en .NET est qu’un type générique fermé n’est effectivement créé que le plus tard possible, lorsqu’il va être utilisé pour la première fois. De plus, il faut bien être conscient que le nombre de classes construites à l’exécution est forcément borné par le nombre de classes génériques fermées utilisées dans le code source.

Un problème similaire plus gênant survient lorsque l’on utilise l’outil ngen.exe pour améliorer les performances globales en effectuant le travail du compilateur JIT avant l’exécution. Dans ce cas, tous les types génériques fermés mentionnés dans votre code sources seront créés. L’outil ngen.exe est d’ailleurs incapable de distinguer si certains types génériques fermés mentionnés dans le code source ne seront jamais utilisés.

Notez enfin que l’article Comparing .NET generics and C++ templates  par Brent Rector donne plus d’information quant à la comparaison des génériques en .NET avec le mécanisme de template de C++.

Visibilité d’un type générique

La visibilité d’un type générique est l’intersection de la visibilité du type générique avec celles de ses types paramètres. Si les visibilités des types C, T1, T2 et T3 sont toutes égales à public alors la visibilité du type C<T1,T2,T3> est public; mais si la visibilité d’un seul de ces types est private, alors la visibilité du type C<T1,T2,T3> est private.

Le lecteur astucieux subodore déjà que l’on peut obtenir un type générique avec une visibilité jusqu’ici inconnue en C# mais connue du CLR qui est protected and internal (visible seulement dans les classes dérivées situées dans le même assemblage). Un tel type est forcément construit à l’exécution par le CLR, et cela n’entraînant donc aucune incohérence dans la langage C#.

internal class ClassInternal { }

public class ClassFoo{

   protected class ClassProtected { }

   public class ClassPublic<U,V> { }

 

   // Le compilateur vérifie que le type ClassPublic<ClassInternal,ClassProtected> n’est pas utilisé ailleurs que

   // dans cette classe et dans ses classes dérivée définie dans le même composant, mais vous ne pouvez fournir

   // une autre visibilité que private pour ce champ.

   private ClassPublic<ClassInternal,ClassProtected> foo;

}

Structures et interfaces génériques

En plus des classes génériques, C#2 permet de définir des structures et des interfaces génériques. Ces possibilités n’ajoutent pas de remarques particulières mis à part le fait qu’un type ne peut implémenter plusieurs fois la même interface générique avec des types paramètres différents. Concrètement, le programme suivant ne compile pas :

interface I<T> { void Fct(); }

// Erreur de compilation:

// 'C<U,V>' cannot implement both 'I<U>' and 'I<V>' because they may unify for some type parameter substitutions

class C<U, V> : I<U>, I<V>{

   void I<U>.Fct() { }

   void I<V>.Fct() { }

}

Possibilité de créer des alias sur le nom d’un type générique fermé

La directive using peut être utilisée pour créer un alias sur le nom d’un type générique fermé. La portée d’une telle directive est le fichier courant si elle est utilisée hors de tous espace de noms, sinon la portée est l’intersection entre le fichier courant et l’espace de noms dans lequel l’alias est défini. Par exemple :

using Annuaire = Dictionary<TelephoneNumber, string>;

class TelephoneNumber { }

class Dictionary<K, V>{ }

...

Annuaire annuaire = new Annuaire();

Possibilité de contraindre un type paramètre

C#2 présente la possibilité d’imposer des contraintes sur un type paramètre d’un type générique. Sans cette possibilité, la généricité de C#2 ne serait pratiquement pas utilisable. En effet, on ne peut pratiquement rien faire avec un type paramètre sur lequel on ne connaît rien. On ne sait même pas s’il est instanciable (puisqu’il peut prendre la forme d’une interface). De plus, on ne peut pas appeler une méthode particulière sur une instance d’un tel type, on ne peut pas comparer les instances d’un tel type etc

Pour pouvoir utiliser un type paramètre au sein d’un type générique, vous pouvez lui apposer une ou plusieurs contraintes parmi trois sortes de contraintes:

·         La contrainte d’avoir un constructeur par défaut.

·         La contrainte d’implémenter une certaine interface ou (non exclusif) de dériver d’une certaine classe.

·         La contrainte d’être un type valeur ou (exclusif) un type référence.

Notez que le mécanisme de template de C++ n’a pas besoin de contraintes pour exploiter les types paramètres puisque les types paramètres sont forcément résolus au moment de la compilation. Dans ce cas, toute tentative d’utilisation d’un membre absent est donc détectée à la compilation.

La contrainte du constructeur par défaut

Si vous souhaitez pouvoir instancier un type paramètre au sein d’un type générique vous n’avez pas d’autre choix que de lui apposer une contrainte du constructeur par défaut. Voici un exemple qui illustre la syntaxe:

class Factory<U> where U: new(){

   public static U GetNew(){ return new U(); }

}

class Program                          {

   static void Main(string[] args){

      int i = Factory<int>.GetNew();

      object o = Factory<object>.GetNew();

      // ici i vaut 0 et o est une instance de object

   }

}

Contraintes de dérivation

Si vous souhaitez utiliser certains membres des instances d’un type paramètre au sein d’un type générique, vous devez lui apposer une contrainte de dérivation. Voici un exemple qui illustre la syntaxe :

interface ICustomInterface{int Fct();}

class C<U> where U : ICustomInterface{

   public int AutreFct(U u){return u.Fct();}

}

Vous pouvez apposer plusieurs contraintes d’implémentation d’interfaces et une contrainte de dérivation d’une classe de base sur un même type paramètre. Le cas échéant, la classe de base doit apparaître en premier dans la liste des types. Vous pouvez aussi utiliser conjointement la contrainte du constructeur par défaut avec une ou plusieurs contraintes de dérivations. Dans ce cas la contrainte du constructeur par défaut doit apparaître en dernier :

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();}

}

Vous ne pouvez pas utiliser une classe sealed ou une des classes System.Object, System.Array, System.Delegate, System.Enum ou System.ValueType comme classe de base pour un type paramètre.

Vous ne pouvez pas non plus utiliser les membres statiques de T comme ceci :

class BaseClass { public static void Fct(){} }

class C<T> where T : BaseClass{

   void F(){ 

      // Erreur de compilation: 'T' is a 'type parameter', which is not valid in the given context

      T.Fct();

   }

}

Un type utilisé dans une contrainte de dérivation peut être un type générique ouvert ou fermé. Illustrons cette possibilité avec l’interface System.IComparable<T>. Rappelons que les types qui implémentent cette interface peuvent voir leurs instances comparées à une instance de type T.

class C1<U> where U : System.IComparable<int>{

   public bool Egaux(U u,int i) { return u.Equals(i); }

}

class C2<U> where U : System.IComparable<U>{

   public int Compare(U u1,U u2){return u1.CompareTo(u2);}

}

class C3<U,V> where U : System.IComparable<V>{

   public int Compare(U u, V v){return u.CompareTo(v);}

}

class C4<U, V> where U : System.IComparable<V>, System.IComparable<int>{

   public int Compare(U u, int i) { return u.CompareTo(i); }

}

Notez qu’un type utilisé dans une contrainte de dérivation doit avoir une visibilité égale ou supérieure à celle du type générique qui contient le type paramètre concerné. Par exemple :

internal class BaseClass{}

// Erreur de compilation: Inconsistent accessibility: constraint type 'BaseClass' is less accessible than 'C<T>'

public class C<T> where T : BaseClass{}

Pour pouvoir être exploitées dans un type générique, certaines fonctionnalités peuvent vous obliger à apposer certaines contraintes de dérivation. Par exemple si vous souhaitez utiliser un type paramètre T dans une clause catch, vous devez contraindre T à dériver de la classe System.Exception ou d’une de ses classes dérivées. De même si vous souhaitez utiliser le mot-clé using pour disposer automatiquement une instance d’un type paramètre, celui-ci doit être contraint d’implémenter l’interface System.IDisposable. Enfin, si vous souhaitez utiliser le mot-clé foreach pour énumérer les éléments d’une instance d’un type paramètre, celui-ci doit être contraint d’implémenter l’interface System.Collections.IEnumerable ou System.Collections.Generic.IEnumerable<T>.

Notons enfin que dans le cas particulier où T est contraint d'implémenter une interface et T est un type valeur, l'appel d'un membre de l'interface sur une instance de T ne provoque pas de boxing. L'exemple suivant met en évidence ce phénomène:

interface ICompteur{

   void Increment();

   int Val{get;}

}

struct Compteur : ICompteur{

   private int i;

   public void Increment() { i++; }

   public int Val { get { return i; } }

}

class C<T> where T : ICompteur, new(){

   public void Fct(){

      T t = new T();

      t.Increment();   // Modifie l'état de t

      System.Console.WriteLine(t.Val.ToString());

      ((ICompteur)t).Increment(); // Modifie l'état d'une copie boxée de t

      System.Console.WriteLine(t.Val.ToString());

   }

}

class Program{

   static void Main(string[] args){

      C<Compteur> c = new C<Compteur>();

      c.Fct();

      System.Console.ReadLine();

   }

}

Ce programme affiche :

1

1

La contrainte type valeur/type référence

La contrainte type valeur/type référence permet de contraindre un type paramètre à être un type valeur ou un type référence. Cette contrainte, qui doit être utilisée en premier dans la liste des contraintes sur un type paramètre donné, utilise les mots-clés struct pour contraindre un type valeur et class pour contraindre un type référence. Attention, cette syntaxe peut prêter à confusion puisque les classes représentent un sous ensemble des types références (il y a aussi les interfaces) et les structures représentent un sous ensemble des types valeurs (il y a aussi les énumérations). Cette contrainte peut être utile dans certains cas particuliers où l'on souhaite utiliser des tests de nullité de références (une instance de type valeur ne peut jamais être nulle) ou lorsque l'on veut s'assurer qu'un type paramètre utilisé avec le mot-clé lock est de type référence.

class C<U> where U : class, new (){

   U u = new U();

   void Fct(){

      lock(u){

      }

   }

}

Les contraintes qui vont manquer dans C#2

Bien que ce mécanisme inédit de contrainte soit très séduisant on peut regretter que :

·         Le mécanisme de constructeur par défaut ne soit pas étendu à toute sorte de constructeur (en fournissant la signature après le mot-clé new par exemple).

·         Il n’y a pas de contrainte de type numérique (int, float, double …). Cela aurait été très utile pour implémenter des types de nombres complexes, de calcul vectoriel…

Les concepteurs du langage approuvent ces remarques mais ils manquent de temps pour les ajouter dans la version 2 de C#. On peut donc s’attendre à retrouver ces contraintes dans une future version.

Les membres d'un type générique

Surcharge de méthode

Les propriétés, les constructeurs, les méthodes et les indexeurs peuvent être surchargés dans une classe générique. Cependant il peut y avoir ambiguïté lorsqu'une certaine combinaison des types paramètres amène plusieurs surcharges à avoir une même signature. Dans ce cas, la préférence ira à la surcharge qui a la signature avec le moins de types paramètres. Si une telle méthode ne peut être trouvée, alors le compilateur émet une erreur au niveau de l'appel ambiguë. 

Voici un petit programme pour clarifier tout ceci:

interface I1<T> {}

interface I2<T> {}

class C1<U>{

   public void Fct1(U u){}              // Cette fct ne peut être appelée si U est i

   public void Fct1(int i){}            

 

   public void Fct2(U u1, U u2){}       // Pas d'ambiguité

   public void Fct2(int i, string s){} 

 

   public void Fct3(I1<U> a){}          // Pas d'ambiguité

   public void Fct3(I2<U> a){}

 

   public void Fct4(U a){}              // Pas d'ambiguité

   public void Fct4(U[] a){}

}

class C2<U,V>{

   public void Fct5(U u, V v){}         // Possibilité d'ambiguité si

   public void Fct5(V v, U u){}         // le type U = le type V

 

   public void Fct6(U u, V v){}         // Possibilité d'ambiguité si

   public void Fct6(V v, U u){}         // le type U = le type V != int

   public void Fct6(int u, V v){}

 

   public void Fct7(int u, V v){}       // Possibilité d'ambiguité si

   public void Fct7(U u, int v){}       // le type U = le type V = int

 

   public void Fct8(U u, I1<V> v){}     // Possibilité d'ambiguité

   public void Fct8(I1<V> v, U u){}     // par exemple pour c2<I1<int>,int>

 

   public void Fct9(U u1, I1<V> v2){}   // Pas d'ambiguité

   public void Fct9(V v1, U u2){} 

 

   public void Fct10(ref U u){}         // Pas d'ambiguité

   public void Fct10(out V v){ v = default(V); }

}

class Program{

   static void Main(string[] args){

      C1<int> a = new C1<int>();

      a.Fct1(34); // appelle   Fct1(int i) {

      C2<int, int> b = new C2<int, int>();

      b.Fct5(13, 14);   // Erreur de compilation: This call is ambiguous...

      b.Fct6(13, 14);   // appelle Fct6(int u, V v

      b.Fct7(13, 14);   // Erreur de compilation: This call is ambiguous...

      C2<I1<int>, int> c = new C2<I1<int>, int>();

      c.Fct8(null, null);  // Erreur de compilation: This call is ambiguous...

   }

}

Note : Ces règles ont subi des évolutions durant la conception de C#2 et par exemple l’ouvrage The C# programming language en donne d’autres.

Les champs statiques

Lorsqu’un type générique contient un champ statique, celui-ci existe à l’exécution en autant de versions qu’il y a de types génériques fermés fabriqués à partir du type générique concerné. Cette règle s’applique indépendamment du fait que le type du champ statique est fonction d’un type paramètre ou pas. Cette règle s’applique aussi indépendamment du fait que les types paramètres des types génériques fermés sont des types valeurs ou références. Cette dernière remarque est pertinente car le fait que les types génériques fermés ayant des types paramètres références, se partagent la même implémentation à l’exécution amène à se poser la question.

Tous ceci est illustré par l’exemple suivant :

using System;

class C<T>{

   private static int nInstances = 0;

   public C() { nInstances++; }

   public int NInstances { get { return nInstances; } }

}

class Program{

   static void Main(string[] args){

      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("NInstances C<int>:"+ c1.NInstances.ToString() );

      Console.WriteLine("NInstances C<string>:" + c4.NInstances.ToString());

      Console.WriteLine("NInstances C<object>:" + c6.NInstances.ToString());

      Console.ReadLine();

   }

}

Ce programme affiche :

NInstances C<int>:3

NInstances C<string>:2

NInstances C<object>:1

Les méthodes statiques

Un type générique peut avoir des méthodes statiques. Dans ce cas il est obligatoire de résoudre les types paramètres lors de l’invocation d’une telle méthode. Par exemple :

class C<T>{

   private static T t;

   public static void ChangeState(T t_){this.t = t_;}

}

class Program                          {

   static void Main(string[] args){

      C<int>.ChangeState(5);

   }

}

Notez enfin que la méthode statique Main(), point d’entrée d’un programme, ne peut être dans une classe générique.

Le constructeur statique

Si un type générique contient un constructeur statique, celui-ci est appelé à chaque création d’un de ses types génériques fermés. Nous pouvons exploiter cette propriété pour ajouter nos propres contraintes sur les types paramètres. Par exemple, on ne peut pas strictement contraindre un type paramètre à ne pas être le type int. On peut donc profiter du constructeur statique pour vérifier une telle contrainte comme ceci :

class C<T>{

   static C(){

      int a=0;

      if( ((object) default(T) != null) && a is T)

         throw new ArgumentException("Not allowed to use the type C<int>.");

   }

}

Notez le test de la non nullité de la valeur par défaut de T. En effet, l’expression (a is T) est vrai lorsque T est le type object et lorsque T est le type int. Pour éliminer le premier cas, nous comptons sur le fait que l’expression (object)defaut(object) renvoie la valeur nulle.

Surcharge des opérateurs

Bien que cela puisse mener à du code peu lisible, un type générique peut surcharger les opérateurs. Il n’y a pas de remarques particulières concernant les opérateurs arithmétiques et les opérateurs de comparaisons.

En revanche, lorsque l’on définit un opérateur de conversion (i.e opérateur de transtypage) d’un type source Src vers un type destination Dest, le compilateur ne doit pas pouvoir trouver de relation d’héritage entre les deux types au moment où le type générique est compilé. Par exemple :

class C<T>{}

class D<T> : C<T>{

   public static implicit operator C<int>(D<T> val) { ... }  // OK

   // Erreur de compilation:  '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(string[] args){

      D<int> dd = new D<int>();  // OK

   }

}

Une conséquence du fait que l’on peut redéfinir certains opérateurs de conversion dans un type générique est qu’il devient possible de redéfinir certains opérateurs de conversions de types prédéfinis. Dans l’exemple suivant, si le type paramétré U est le type objet nous redéfinissons l’opérateur implicite de conversion de D<object> vers object :

class D<U> {

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

}

Dans ce cas deux règles sont appliquées par le CLR :

·         Si une conversion implicite prédéfinie existe du type Src vers le type Dest, alors toute redéfinition (implicite ou explicite) de cette conversion est ignorée.

·         Si une conversion explicite prédéfinie existe du type Src vers le type Dest, alors toute redéfinition de cette conversion est ignorée. En revanche, les redéfinitions implicites de la conversion du type Src vers le type Dest sont utilisées.

Les types encapsulés

Un type encapsulé dans un type générique est implicitement un type générique. Les types paramètres du type générique encapsulant peuvent être librement utilisés. Un type encapsulé dans un type générique a la possibilité d’avoir ses propres types paramètres. Dans ce cas il y aura un type encapsulé générique fermé construit par le CLR pour chaque combinaison différente utilisée de l’ensemble des types paramètres.

class Outer<U>{

   static Outer(){System.Console.WriteLine("Hello du cctor de Inner.");}

   public class Inner<V>{

      static Inner(){System.Console.WriteLine("Hello du cctor de Inner.");}

   }

}

class Program{

   static void Main(string[] args){

      Outer<string>.Inner<int> a = new Outer<string>.Inner<int>();

      Outer<int>.Inner<int> b = new Outer<int>.Inner<int>();

      System.Console.ReadLine();

   }

}

Ce programme affiche :

Hello du cctor de Inner.

Hello du cctor de Inner.

Les opérateurs et les types génériques

Utilisation des opérateurs d’égalité, d’inégalité et de comparaison avec une instance d’un type paramètre

Les opérateurs d’égalité et d’inégalité ne peuvent être utilisé avec une instance ou une référence d’un type paramètre T que dans les cas suivants:

·         Si T a une contrainte de dérivation d’une classe ou T a une contrainte de type référence, alors les opérateurs d’égalité et d’inégalité peuvent être utilisés entre une référence de type T et n’importe qu’elle référence.

·         Si T n’a pas de contrainte de type valeur, alors les opérateurs d’égalité et d’inégalité peuvent être utilisés entre une référence de type T et la référence null. SI T prend la forme d’un type valeur, le test d’égalité sera faux et le test d’inégalité sera vrai.

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) { } // Erreur de compilation

      if (v == o) { } // Erreur de compilation

      if (v == i) { } // Erreur de compilation

      if (u == null) { } // OK

      if (v == null) { } // Erreur de compilation

   }

   public void Fct2(T t1, U u1, V v1, T t2, U u2, V v2){

      if (t1 == t2) { } // OK

      if (u1 == u2) { } // Erreur de compilation

      if (v1 == v2) { } // Erreur de compilation

   }

}

Les opérateurs de comparaisons ne peuvent jamais être utilisés avec une instance ou une référence d’un type paramètre T.

L’opérateur typeof

L’opérateur typeof utilisé sur un type paramètre retourne l’instance du type Type correspondant à la valeur courante du type paramètre.

L’opérateur typeof utilisé sur un type générique retourne l’instance du type Type correspondant à l’ensemble des valeurs courantes des types paramètres. Ce comportement n’est pas flagrant puisque dans la version beta 1 les propriété Name et FullName des types retournés n’affichent pas les noms des types paramètres.

using System;

class C<T>{

   public static void PrintTypes(){

      Console.WriteLine(typeof(T).Name);

      Console.WriteLine(typeof(C<T>).Name);

      Console.WriteLine(typeof(C<C<T>>).Name);

      if(typeof(C<T>) != typeof(C<C<T>>))

         Console.WriteLine("Malgré un nom similaire ce ne sont pas les mêmes instances de Type);

   }

}

class Program                          {

   static void Main(string[] args){

      C<string>.PrintTypes();

      C<int>.PrintTypes();

      Console.ReadLine();

   }

}

Ce programme affiche:

String

C`1

C`1

Malgré un nom similaire ce ne sont pas les mêmes instances de Type.

Int32

C`1

C`1

Malgré un nom similaire ce ne sont pas les mêmes instances de Type.

Les mots clés params et lock

Un type paramètre peut être utilisé comme type pour un paramètre params de la signature d’une méthode d’un type générique.

Le mot clé lock peut être utilisé avec une variable d’un type paramètre. Cette possibilité présente un danger lorsque le type paramètre est un type valeur. Il faut bien être conscient que dans ce cas, le mot clé lock n’aura aucun effet. Il est d’ailleurs assez surprenant que le compilateur ne force pas un type paramètre utilisé dans une clause lock à avoir une contrainte qui le force à être de type référence.

L’opérateur default

Dans l’exemple de la pile, nous avons considéré l'opération Pop() sur une pile vide comme une erreur d'utilisation de la classe Stack<T> de la part du client (i.e une violation du contrat présenté par une pile). Nous aurions pu affaiblir le contrat et considérer cette opération comme un évènement possible. Dans le premier cas, lancer une exception est le traitement adapté. Dans le second, il serait plus judicieux de retourner un élément vide que le client interprétera comme: il n'y a plus d'élément dans ma pile. Cependant nous ne connaissons rien du type T de l'élément à retourner. Si T est un type référence nous souhaiterions retourner une référence nulle alors que si T est le type int nous souhaiterions peut être retourner 0. Le mot clé default de C#2 permet d'obtenir la valeur par défaut d'un type, i.e la référence nulle pour un type référence ou un block de mémoire de la taille adéquate mis à 0 pour un type valeur.

class Stack<T>{

...

   public T Pop(){

      if (m_Index == 0)

         return default(T);

      return m_ItemsArray[--m_Index];

   }

...

}

Notez que la notion de type nullable constitue une manière plus élégante de définir la valeur par défaut d'un type valeur.

Note : La syntaxe a évolué et certaines sources obsolètes vous présenteront l’ancienne syntaxe T.default. La syntaxe a été changé car elle laissait à penser que default était un champ ou une propriété statique de T, ce qui n’est pas le cas.

Le transtypage (casting) et la généricité

Les règles de base

Dans la suite, nous supposerons que T est un type paramètre. Le compilateur C#2 accepte de:

·         Transtyper implicitement une instance d'un type T (si T est de type valeur sinon une référence de type T) vers une référence de type objet. Si T est de type valeur, il y a une opération de boxing.

·         Transtyper explicitement une référence de type objet vers une instance d'un type T. Si T est de type valeur, il y a une opération de unboxing.

·         Transtyper explicitement une instance d'un type T vers une référence de type une interface quelconque. Si T est de type valeur, il y a une opération de boxing.

·         Transtyper explicitement une référence de type une interface quelconque vers une instance d'un type T. Si T est de type valeur, il y a une opération de boxing.

Dans les trois derniers cas, si le transtypage est impossible, une exception de type InvalidCastException est lancée.

D'autres règles de transtypage s'ajoutent si l'on utilise des contraintes de dérivations:

·         Si T est contraint d'implémenter l'interface I, vous pouvez transtyper implicitement une instance de T en I ou en toute interface implémentée par I et vice versa. Si T est de type valeur, il y a une opération de boxing (ou de unboxing).

·         Si T est contraint de dériver de la classe C, vous pouvez transtyper implicitement une instance de T en C ou en toute sous-classe de C et vice versa. Si une conversion propriétaire implicite existe de C vers A alors le compilateur accepte une conversion implicite de T vers A. Si une conversion propriétaire explicite existe de A vers C alors le compilateur accepte une conversion explicite de A vers T.

Transtypage entre tableaux

Si T est un type paramètre d’une classe générique et si T à la contrainte de dériver de C alors le compilateur C#2 accepte de :

·         Transtyper implicitement un tableau de T en un tableau de C. Autrement dit, le compilateur C#2 accepte de transtyper implicitement une référence de type T[] vers une référence de type C[]. On dit que les tableaux de C# acceptent la covariance sur leurs éléments.

·         Transtyper explicitement un tableau de C en un tableau de T. Autrement dit, le compilateur C#2 accepte de transtyper explicitement une référence de type C[] vers une référence de type T []. On dit que les tableaux de C# acceptent la contravariance sur leurs éléments.

Ces deux règles sont illustrées par l’exemple suivant :

class C { }

class ClassGenerique<T> where T : C{

   T[] arrOfT = new T[10];

   public void Fct(){

      C[] arrOfC = arrOfT;

      T[] arrOfT2 = (T[]) arrOfC;

   }

}

Il n’y a pas de règles équivalentes si T est contraint d’implémenter une interface I.

En outre, la covariance et la contravariance ne sont pas supportées sur les types paramètres d’une classe générique. Autrement dit, si la classe D dérive de la classe B, il n’existe pas de conversion implicite ou explicite entre une référence de type List<D> et une référence de type List<B>. Plus d’information à ce sujet ici.

Les opérateurs is et as

Pour éviter une exception de type InvalidCastException lorsque vous n’êtes pas certain d’une conversion de type impliquant un type paramètre T, il est conseillé d’utiliser l’opérateur is pour tester si la conversion est possible et l’opérateur as pour tenter de réaliser la conversion. Rappelons que l’opérateur as retourne la référence null si la conversion est impossible. Par exemple :

using System.Collections.Generic;

class C<T> {

   public void Fct(T t){

      int i = t as int; // Erreur de compilation: 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 i in enumerable){}

      }

   }

}

L’héritage et la généricité

Les différents cas

Une classe non générique peut dériver d’une classe générique. Dans ce cas tous les types paramètres doivent être résolus :

class B<T> {...}

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

Une classe générique peut dériver d’une classe générique. Dans ce cas il est optionnel de résoudre tous les paramètres. En revanche il est obligatoire de rappeler toutes les contraintes sur les types paramètres non résolus. Par exemple :

class B<T> where T : struct { }

class D1<T> : B<T> where T : struct  { }

class D2<T> : B<int> { }  // maladroit T est ici un type paramétre différent

class D3<U,V> : B<int> { } 

Enfin, une classe générique peut dériver d’une classe non générique.

Surcharge de méthodes virtuelles d’un type générique

Une classe générique de base peut avoir des méthodes abstraites ou virtuelles qui utilisent ou non les types paramètres dans leur signature. Dans ce cas, le compilateur oblige les réécritures de telles méthodes dans les classes dérivées à utiliser les types paramètres adéquates. Par exemple :

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); }

}

// Erreur de compilation : does not implement inherited abstract member 'B<U>.Fct(U)'

class D3<T, U> : B<U> {

   // Erreur de compilation : no suitable method found to override

   public override T Fct(T t) { return default(T); }

}

On profite de l’exemple pour souligner le fait qu’une classe générique peut aussi être abstraite. Cet exemple montre aussi le genre d’erreur de compilation que l’on a lorsque l’on nomme maladroitement les types paramètres.

Il est intéressant de noter que les types paramètres d’une classe générique dérivée peuvent être utilisés dans le corps d’une méthode virtuelle réécrite, même si la classe de base n’est pas générique.

class B  {

   public virtual void Fct() { }

}

class D<T> : B where T : new(){

   public override void Fct() {

      T t = new T();

      ...

   }

}

Toutes les règles énoncées dans la présente section restent valables pour l’implémentation d’interfaces éventuellement génériques, par des classes ou des structures éventuellement génériques.

Les méthodes génériques

Introduction

Qu’elle soit définie dans un type générique ou non, qu’elle soit statique ou non, une méthode a la possibilité de définir ses propres types paramètres. A chaque invocation d’une telle méthode un type doit être fourni pour chaque type paramètre.

Les types paramètres propres à une méthode ne sont utilisables que dans le scope de la méthode (i.e valeur de retour + signature + corps de la méthode). Dans la classe C2<T> de l’exemple suivant, il n’y a pas de corrélation entre le type paramètre U de la méthode Fct<U>() et le type paramètre U de la méthode FctStatic<U>().

Un type paramètre d’une méthode peut avoir le même nom qu’un type paramètre de la classe qui définie la méthode. Dans ce cas, le type paramètre de la classe est caché dans le scope de la méthode. Dans la méthode C3<T>.Fct<T>() de l’exemple suivant, le type paramètre T défini par la méthode cache le type paramètre T défini par la classe. Cette pratique est plutôt maladroite et le compilateur produit un avertissement lorsqu’il la détecte.

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>{

    // Avertissement de compilation : 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(string[] args){

       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);

   }

}

Notez que cette possibilité n’est pas utilisable ni sur les opérateurs, ni sur les méthodes extern ni sur les méthodes particulières que constituent les accesseurs des propriétés, des indexeurs et des évènements.

Méthodes génériques et contraintes

Une méthode générique peut définir toutes sortes de contraintes pour chacun de ses types paramètres. Les mêmes règles s’appliquent que lors de la définition de types paramètres d’un type.

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);

    }

}

Bien évidemment, une méthode générique ne peut redéfinir l’ensemble des contraintes d’un type paramètre défini par sa classe.

Méthodes virtuelles génériques

Les méthodes abstraites, virtuelles et d’interface peuvent être génériques. Dans ce cas, les réécritures de telles méthodes ne sont pas obligées de respecter le nom des types paramètres. Dans le cas d’une réécriture d’une méthode générique virtuelle ou abstraite qui a des contraintes sur ses types paramètres, vous ne devez pas réécrire l’ensemble des contraintes. Dans le cas d’une implémentation d’une méthode d’interface qui a des contraintes sur ses types paramètres, vous devez réécrire l’ensemble des contraintes. Ces deux règles antagonistes seront certainement sujettes à évolution.

abstract class B{

    public virtual A Fct1<A, B>(A a, B b) { return a; }

    public abstract int Fct2<U>(U u) where U : class, System.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, B>(A a, B b);

    int Fct2<U>(U u) where U : class, System.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, System.IComparable<U>, new()  { return 0; }

}

Inférence des types paramètres selon les types des paramètres d’une méthode générique

Lors de l’invocation d’une méthode générique, le compilateur C#2 a la possibilité d’inférer les types paramètres d’une méthode générique à partir des types des paramètres fournis. Notez que le fait de fournir explicitement des types pour les types paramètres prévaut sur les règles d’inférences.

Les règles d’inférences ne tiennent pas comptes du type de la valeur de retour. En revanche le compilateur est capable d’inférer un type paramètre à partir du type des éléments d’un tableau. Le programme suivant illustre tout ceci:

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(string[] args){

       // Erreur de compilation: The type arguments for method 'C.Fct1<U>()' cannot be inferred from the usage.

       string s = C.Fct1();

 

       // Erreur de compilation: Cannot implicitly convert type 'System.IDisposable' to 'string'.

       string s = C.Fct1<System.IDisposable>();

       s = C.Fct1<string>(); // OK

 

       C.Fct2("hello"); // infère: le type paramètre U est string.

 

       // Erreur de compilation: The type arguments for method 'C.Fct2<U>(U)' cannot be inferred from the usage.

       C.Fct2(null);

 

       int i = C.Fct3(6);// infère: le type paramètre U est int.

 

       double d = C.Fct3(6); // ATTENTION: infère: le type paramètre U est int et non pas double.

 

       // Erreur de compilation: Cannot implicitly convert 'int' to 'System.IDisposable'.

       System.IDisposable dispose = C.Fct3(6);

 

       C.Fct4("hello", "bonjour"); // infère: le type paramètre U est string.

 

       // Erreur de compilation: 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]); // infère: le type paramètre U est int.

   }

}

Ambiguïté dans la grammaire de C#2

En cherchant bien, on trouve une ambiguïté dans la grammaire de C#2 car les caractères inférieur ‘<’ et supérieurs ‘>’ peuvent dans certains cas très précis être interprétés à la fois comme une la définition d’une liste de types paramètres et deux utilisations de l’opérateurs de comparaison. Ce cas ‘tiré par les cheveux’ est illustré par l’exemple suivant :

class C<U,V>

{

    public static void Fct1()

    {  

        int U = 6;

        int V = 7;

        int Fct2 = 9;

        Fct3(Fct2<U, V>(20)); // appèle Fct3(int)

        Fct3(Fct2<U, V>20); // appèle 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; }

}

La règle est que lorsque le compilateur rencontre un tel dilemme, il analyse le caractère situé immédiatement après ‘>’. Si celui-ci est dans la liste suivante, alors le compilateur infère une liste de type paramètre :

(  )  ]  >  :  ;  ,  .  ?

Les délégués, les évènements et la généricité

Introduction

Comme tous les types encapsulés, une délégation (i.e une classe dont les instances sont des délégués) peut exploiter les types paramètres du type qui l’encapsule :

class C<T>{

    public delegate T GenericDelegate(T t);

    public static T Fct(T t) { return t; }

}

class Program{

   static void Main(string[] args){

       C<string>.GenericDelegate genericDelegate = C<string>.Fct;

       string s = genericDelegate("hello");

   }

}

L’exemple précédent expose aussi la nouvelle syntaxe d’assignement d’un délégué où l’on n’a plus besoin de préciser explicitement l’appel au constructeur comme ceci:

...

     C<string>.GenericDelegate genericDelegate = new C<string>.GenericDelegate( C<string>.Fct );

...

En outre, une délégation peut aussi définir ses propres types paramètres ainsi que leurs contraintes :

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(string[] args){

       GenericDelegate<string> genericDelegate = C<string>.Fct;

       string s = genericDelegate("hello");

   }

}

Délégués génériques et méthodes génériques

Lors d’une affectation d’une méthode générique à un délégué générique, le compilateur C#2 est capable d’inférer les types paramètres de la méthode générique à partir des types paramètres du délégué générique. Cette possibilité est illustrée par l’exemple suivant :

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(string[] args){

       GenericDelegateA<string> d1 = Fct1; // Le compilateur infére Fct1<string>

       GenericDelegateB d2 = Fct1; // Le compilateur infére Fct1<int>

       GenericDelegateC<string> d3 = Fct2<string>; // OK mais pas d'inférence

       // Erreur de compilation: The type arguments for method 'Program.Fct2<T>()' cannot be inferred from the usage.

       GenericDelegateC<string> d4 = Fct2;

   }

}

Notez qu’il n’y a jamais d’inférence sur les types paramètres d’un délégué générique.

Contravariance, covariance, délégués et généricité

Nous exposons ici une nouvelle fonctionnalité des délégués initialement soulignée dans le blog de Wesner Moise et qui va nous être utile par la suite. En C#2, les délégués supportent la contravariance sur leurs arguments et la covariance sur leur type de retour. Cette possibilité est exposée ci dessous :

class Base { }

class Derived : Base { }

delegate Base DelegateType(Derived d);

class Program{

    static Derived Handler(Base b){return b as Derived;}

    static void Main(string[] args){

        DelegateType delegateInstance = Handler;

        Base b = delegateInstance(new Derived());