|
|
Le support de la généricité par
C#2 par Patrick
Smacchia |
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
Résolution
élégante du problème à l’aide d’une classe générique de C#2
Vue
d’ensemble de la généricité de C#2
Possibilité
pour un type d’être génériques sur plusieurs types
Types
génériques ouverts et fermés
La
généricité de .NET vs. le mécanisme de templates de C++
Visibilité
d’un type générique
Structures
et interfaces génériques
Possibilité
de créer des alias sur le nom d’un type générique fermé
Possibilité
de contraindre un type paramètre
La
contrainte du constructeur par défaut
La
contrainte type valeur/type référence.
Les
contraintes qui vont manquer dans C#2.
Les
membres d'un type générique
Les
opérateurs et les types génériques
Le
transtypage (casting) et la généricité.
Surcharge
de méthodes virtuelles d’un type générique
Méthodes
génériques et contraintes
Méthodes
virtuelles génériques
Inférence
des types paramètres selon les types des paramètres d’une méthode
générique
Ambiguïté
dans la grammaire de C#2
Les
délégués, les évènements et la généricité
Délégués
génériques et méthodes génériques
Contravariance,
covariance, délégués et généricité
Evènements
et délégués génériques
Réflexion,
attribut, IL et généricité
Evolution
de la classe System.Type
Evolution
des classes System.Reflection.MethodBase et System.Reflection.MethodInfo
Les
attributs et la généricité
La
généricité et le langage IL
La
généricité et le framework .NET
La
sérialisation et la généricité
.NET
Remoting et la généricité
Les
collections et la généricité
Les
domaines ne supportant pas la généricité
Une
bonne connaissance générale de C#1.
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é.
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 1234L_000c: box int32L_0011: callvirt instance void Stack::Push(object)L_0016: nop L_0017: ldloc.0 L_0018: callvirt instance object Stack::Pop()L_001d: unbox int32L_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.
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>, 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();
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
int i = Factory<int>.GetNew();
object o = Factory<object>.GetNew();
// ici i vaut 0 et o est une
instance de object
}
}
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
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 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
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.
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
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
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
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.
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.
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
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.
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
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 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
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.
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.
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){}
}
}
}
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.
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
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
// 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 :
( )
] > : ; , . ?
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
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
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
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
DelegateType delegateInstance = Handler;
Base b = delegateInstance(new
Derived());