Visual Studio 2005 : FAQ sur les génériques : notions fondamentales

Par Juval Lowy

Octobre 2005

Lire cet article en anglais  Aa479859.us(fr-fr,MSDN.10).gif

Concerne :
types génériques

Résumé : examinez les questions fréquentes concernant les types génériques et leurs différentes utilisations. (51 pages imprimées)

Sur cette page

Qu’est-ce qu’un type générique ?
Qu'est-ce qu'un paramètre de type générique ?
Qu'est-ce qu'un argument de type générique ?
Qu'est-ce qu'un type construit ?
Qu'est-ce qu'un type construit ouvert ?
Qu'est-ce qu'un type construit fermé ?
Comment utiliser un type générique ?
Comment initialiser un paramètre de type générique ?
Quels sont les avantages des génériques ?
Pourquoi ne puis-je pas utiliser de structures de données propres au type plutôt que des génériques ?
Dans quels cas dois-je utiliser des génériques ?
Les génériques sont-ils covariants, contra-variants ou invariants ?
Qu'est-ce qui peut définir des paramètres de type générique ? Quels types peuvent être génériques ?
Des méthodes peuvent-elles définir des paramètres de type générique ? Comment appeler ces méthodes ?
Peut-on dériver une classe à partir d'un paramètre de type générique ?
Qu'est-ce qu'une interférence de type générique ?
Que sont les contraintes ?
Avec quoi ne puis-je pas utiliser de contraintes ?
Pourquoi ne puis-je pas utiliser de classes Enum, Struct ou Sealed en tant que contraintes génériques ?
Le code qui utilise les génériques est-il plus rapide que le code qui ne les utilise pas ?
Une application qui utilise les génériques est-elle plus rapide qu'une application qui ne les utilise pas ?
En quoi les génériques sont-ils similaires aux modèles Visual C++ classiques ?
En quoi les génériques sont-ils différents des modèles Visual C++ classiques ?
Quelle est la différence entre l'utilisation de génériques et l'utilisation d'interfaces (ou de classes abstraites) ?
Comment les génériques sont-ils implémentés ?
Pourquoi ne puis-je pas utiliser d'opérateurs sur les paramètres de type générique naked ?
Pourquoi puis-je utiliser des opérateurs sur les paramètres de type générique ?
Puis-je utiliser des attributs génériques ?
Les génériques sont-ils compatibles CLS ?

Qu’est-ce qu’un type générique ?

Un type générique est un type qui utilise des paramètres de type générique. Par exemple, le type LinkedList&#139K;,T›, défini comme :

[C#]

public class LinkedList&#139K;,T›
{...}

[Visual Basic]

Public Class LinkedList(Of K, T)
    ...
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{...};

est un type générique, car il utilise les paramètres de type générique K et T, où K est la clé de la liste et T est le type de la donnée stockée dans la liste. Les types génériques ont ceci de particulier que vous les codez une fois, mais que vous pouvez les utiliser avec différents paramètres. Les avantages sont significatifs : vous réutilisez les efforts de développement et de test sans compromettre la sécurité et les performances du type, et sans gonfler inutilement le code.

Qu'est-ce qu'un paramètre de type générique ?

Un paramètre de type générique est l'espace réservé utilisé par un type générique. Par exemple, le type générique LinkedList&#139K;,T›, défini comme :

[C#]

public class LinkedList&#139K;,T›
{...}

[Visual Basic]

Public Class LinkedList(Of K, T)
    ...
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{...};

utilise deux paramètres de type (K et T), où K est la clé de la liste et T est le type de la donnée stockée dans la liste. L'utilisation de paramètres de type générique permet à la liste liée de différer la décision quant aux types réels à utiliser. En fait, c'est au client de la liste liée générique de spécifier les paramètres de type générique à utiliser.

Qu'est-ce qu'un argument de type générique ?

Un argument de type générique est le type spécifié par le client à utiliser à la place du paramètre de type. Par exemple, sur la base de la définition et de la déclaration de type suivante :

[C#]

public class MyClass&#139T;›
{...}
MyClass&#139int;› obj = new  MyClass&#139int;›();

[Visual Basic]

Public Class SomeClass(Of T)
    ...
End Class
Dim obj As New SomeClass(Of Integer)

[C++]

generic &#139typename; T›
public ref class MyClass
{...};
MyClass&#139int;› ^obj = gcnew  MyClass&#139int;›

T est le paramètre de type, tandis que integer est l'argument de type.

Qu'est-ce qu'un type construit ?

Un type construit est un type générique possédant au moins un argument de type.

Par exemple, à partir de cette définition de liste liée générique :

[C#]

public class LinkedList&#139T;›
{...}

[Visual Basic]

Public Class LinkedList(Of T)
    ...
End Class

[C++]

generic &#139typename; T›
public ref class LinkedList
{...};

Ce qui suit est un type générique construit :

[C#]

LinkedList&#139string;>

[Visual Basic]

LinkedList(Of String)

[C++]

LinkedList&#139String; ^>

Pour qualifier un type en tant que type construit, vous pouvez également spécifier des paramètres de type pour le type générique :

[C#]

public class MyClass&#139T;›
{
   LinkedList&#139T;› m_List; //Constructed type 
}

[Visual Basic]

Public Class SomeClass(Of T)
    Dim m_List As LinkedList(Of T) ' Constructed type
End Class

[C++]

generic &#139typename; T›
public ref class MyClass
{
   LinkedList&#139T;› ^m_List; //Constructed type 
};

Qu'est-ce qu'un type construit ouvert ?

Un type construit ouvert est un type générique contenant au moins un paramètre de type utilisé en tant qu'argument de type. Par exemple, à partir de cette définition :

[C#]

public class LinkedList&#139K;,T›
{...}

[Visual Basic]

Public Class LinkedList(Of K, T)
    ...
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{...};

Les déclarations suivantes des variables membres LinkedList&#139K;,T› sont toutes des types construits ouverts :

[C#]

public class MyClass&#139K;,T›
{
   LinkedList&#139K;,T›      m_List1; //Open constructed type 
   LinkedList&#139K;,string› m_List2; //Open constructed type 
   LinkedList&#139int;,T›    m_List3; //Open constructed type 
}

[Visual Basic]

Public Class SomeClass(Of K, T)
    Dim m_List1 As LinkedList(Of K, T)      'Open constructed type
    Dim m_List2 As LinkedList(Of K, String) 'Open constructed type
    Dim m_List3 As LinkedList(Of Integer, T)'Open constructed type
End Class

[C++]

generic &#139typename; K, typename T›
public ref class MyClass
{
   LinkedList&#139K;, T›      ^m_List1; //Open constructed type 
   LinkedList&#139K;, String ^> ^m_List2; //Open constructed type 
   LinkedList&#139int;, T>    ^m_List3; //Open constructed type 
};

Qu'est-ce qu'un type construit fermé ?

Un type construit fermé est un type générique qui ne contient pas de paramètres de type en tant qu'arguments. Par exemple, à partir de cette définition :

[C#]

public class LinkedList&#139K;,T›
{...}

[Visual Basic]

Public Class LinkedList(Of K, T)
    ...
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{...};

Les déclarations suivantes des variables membres LinkedList&#139K;,T› sont toutes des types construits fermés :

[C#]

LinkedList&#139int;,string> list1; //Closed constructed type 
LinkedList&#139int;,int>    list2; //Closed constructed type 

[Visual Basic]

Dim list1 As LinkedList(Of Integer, String) 'Closed constructed type
Dim list2 As LinkedList(Of Integer, Integer)'Closed constructed type

[C++]

LinkedList&#139int;, String ^> ^list1; //Closed constructed type 
LinkedList&#139int;, int>      ^list2; //Closed constructed type 

Comment utiliser un type générique ?

ou

Comment initialiser un paramètre de type générique ?

Lors de la déclaration d'un type générique, vous devez spécifier les types qui remplaceront les paramètres de type dans la déclaration. Ceux-ci sont connus comme arguments de type pour le type générique. Les arguments de type sont simplement des types. Par exemple, lors de l'utilisation de cette liste liée générique :

[C#]

public class LinkedList&#139K;,T›
{
   public void AddHead(K key,T item);
   //Rest of the implementation 
}

[Visual Basic]

Public Class LinkedList(Of K, T)
    Public Sub AddHead(ByVal key As K, ByVal item As T)
    'Rest of the implementation
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{
   public: void AddHead(K key,T item);
   //Rest of the implementation 
};

Vous devez spécifier les types à utiliser pour K (la clé de la liste) et T (les données stockées dans la liste). Vous spécifiez les types à deux emplacements : lors de la déclaration de la variable de la liste et lors de son instanciation :

[C#]

LinkedList&#139int;,string> list = new LinkedList&#139int;,string>();
list.AddHead(123,"ABC");

[Visual Basic]

Dim list As New LinkedList(Of Integer, String)
list.AddHead(123, "ABC")

[C++]

LinkedList&#139int;, String ^> ^list = gcnew LinkedList&#139int;, String ^>;
list->AddHead(123,"ABC");

Une fois que vous avez spécifié les types à utiliser, vous pouvez simplement appeler des méthodes sur le type générique, en fournissant les valeurs appropriées des types spécifiés précédemment.

Un type générique qui comporte déjà des arguments de type, tel que LinkedList&#139int;,string>, est appelé type construit.

Lors de la spécification des arguments de type pour les types génériques, vous pouvez en fait fournir des paramètres de type. Par exemple, examinez cette définition de la classe Node&#139K;,T›, utilisée en tant que noeud dans une liste liée :

[C#]

class Node&#139K;,T›
{
   public K Key;
   public T Item;
   public Node&#139K;,T› NextNode;

   public Node(K key,T item,Node&#139K;,T› nextNode)
   {
      Key      = key;
      Item     = item;
      NextNode = nextNode;
   }
}

[Visual Basic]

Class Node(Of K, T)
    Public Key As K
    Public Item As T
    Public NextNode As Node(Of K, T)
    Public Sub Node(ByVal key As K, ByVal item As T, ByVal nextNode As Node(Of K, T))
        Me.Key = key
        Me.Item = item
        Me.NextNode = nextNode
    End Sub
End Class

[C++]

generic &#139typename; K, typename T›
ref class Node
{
public: 
   K Key;
   T Item;
   Node&#139K;,T› ^NextNode;

   Node(K key,T item,Node&#139K;,T› ^nextNode)
   {
      Key      = key;
      Item     = item;
      NextNode = nextNode;
   }
};

La classe Node&#139K;,T› contient en tant que variable membre une référence au noeud suivant. Ce membre doit être fourni avec le type à utiliser plutôt que ses paramètres de type générique. Le noeud spécifie ses propres paramètres de type dans ce cas.

Un autre exemple de spécification des paramètres de type générique est la façon dont la liste liée proprement dite peut déclarer et utiliser le noeud :

[C#]

public class LinkedList&#139K;,T›
{
   Node&#139K;,T› m_Head;   

   public void AddHead(K key,T item)
   {...}
}

[Visual Basic]

Public Class LinkedList(Of K, T)
    Dim m_Head As Node(Of K, T)
    Public Sub AddHead(ByVal key As K, ByVal item As T)
        ...
    End Sub
End Class

[C++]

generic &#139typename; K, typename T›
public ref class LinkedList
{
   Node&#139K;,T› ^m_Head;   

   public: void AddHead(K key,T item)
   {...}
};

Notez que l'utilisation de K et T dans la liste liée comme noms des arguments de type est destinée uniquement à la lisibilité, afin de rendre plus cohérente l'utilisation du noeud. Vous auriez pu définir la liste liée avec d'autres noms de paramètre de type générique, auquel cas vous auriez également dû les passer au noeud :

[C#]

public class LinkedList&#139Key;,Item>
{
   Node&#139Key;,Item> m_Head;   

   public void AddHead(Key key,Item item)
   {...}
}

[Visual Basic]

Public Class LinkedList(Of Key, Item)
    Dim m_Head As Node(Of Key, Item)
    Public Sub AddHead(ByVal key As Key, ByVal item As Item)
        ...
    End Sub
End Class

[C++]

generic &#139typename; Key, typename Item>
public ref class LinkedList
{
   Node&#139Key;,Item> ^m_Head;   
   public: void AddHead(Key key,Item item) {...}
};

Quels sont les avantages des génériques ?

Sans les génériques, si vous souhaitez développer des structures de données, collections ou classes d'utilitaire générales, vous devez les baser sur un objet. Par exemple, voici l'interface IList basée sur un objet, trouvée dans l'espace de noms System.Collections :

[C#]

public interface IList : ICollection
{
   int Add(object value);
   bool Contains(object value);
   int IndexOf(object value);
   void Insert(int index, object value);
   void Remove(object value);
   object this[int index]{ get; set; }
   //Additional members 
}

[Visual Basic]

Public Interface IList
    Inherits ICollection

    Function add(ByVal value As Object) As Integer
    Function contains(ByVal value As Object) As Boolean
    Sub insert(ByVal index As Integer, ByVal value As Object)
    Sub Remove(ByVal value As Object)
    Property Item(ByVal index As Integer) As Object
    'Additional members
End Interface

[C++]

public interface class IList : ICollection, IEnumerable
{
   int Add(Object ^value);
   bool Contains(Object ^value);
   void Insert(int index, Object ^value);
   void Remove(Object ^value);
   property Object ^ default[]
   { 
      Object ^ get(int index); 
      void set(int index, Object ^value); 
   }
   //Additional members 
};

Les clients de cette interface peuvent l'utiliser pour manipuler des listes liées de n'importe quel type, y compris des types de valeur tels que des entiers, ou des types de référence tels que des chaînes :

[C#]

IList numbers = new ArrayList();
numbers.Add(1); //Boxing
int number = (int)numbers[0];//Unboxing

IList names = new ArrayList();
names.Add("Bill");
string name = (string)names[0];//Casting

[Visual Basic]

Dim numbers As IList = New ArrayList()
numbers.Add(1) 'Boxing
Dim number As Integer = CType(numbers(0), Integer) 'Unboxing
Dim names As IList = New ArrayList()
names.Add("Bill")
Dim name As String = CType(names(0), String) 'Casting

[C++]

IList ^numbers = gcnew ArrayList;
numbers->Add(1); //Boxing
int number = (int)numbers[0]; //Unboxing

IList ^names = gcnew ArrayList;
names->Add("Bill");
String ^name = (String ^)names[0]; //Casting

Cependant, étant donné que l'interface IList est basée sur un objet, chaque utilisation d'un type de valeur impose son inclusion dans un objet, et son extraction lors de l'utilisation de l'indexeur. L'utilisation de types de référence force l'utilisation d'une conversion qui complique le code et qui affecte les performances.

Examinez à présent l'interface générique équivalente, IList&#139T;›, dans l'espace de noms System.Collections.Generic :
[C#]

public interface IList&#139T;› : ICollection&#139T;›
{
   int IndexOf(T item);
   void Insert(int index, T item);
   void RemoveAt(int index);
   T this[int index]{ get; set; }
}

[Visual Basic]

&#139DefaultMember;("Item")> _
Public Interface IList(Of T)
    Inherits ICollection(Of T)

    Function IndexOf(ByVal item As T) As Integer
    Sub Insert(ByVal index As Integer, ByVal item As T)
    Sub RemoveAt(ByVal index As Integer)
    Property Item(ByVal index As Integer) As T
End Interface

[C++]

generic &#139typename; T›
public interface class IList : ICollection&#139T;›
{
   int IndexOf(T item);
   void Insert(int index, T item);
   void RemoveAt(int index);
   property T default[]   
   { 
      T get(int index); 
     void set(int index, T value); 
   }
   //Additional members 
};

Les clients de cette interface IList&#139T;› peuvent également l'utiliser pour manipuler des listes liées de n'importe quel type, mais sans dégradation des performances. Lors de l'utilisation d'un type de valeur au lieu des paramètres de type, aucune inclusion ou extraction n'est effectuée, et lors de l'utilisation d'un type de référence, aucune conversion n'est requise :

[C#]

IList&#139int;› numbers = new List&#139int;›();
numbers.Add(1);
int number = numbers[0];

IList&#139string;> names = new List&#139string;>();
names.Add("Bill");
string name = names[0];

[Visual Basic]

Dim numbers As IList(Of Integer) = New List(Of Integer)()
numbers.Add(1)
Dim number As Integer = numbers(0)

Dim names As IList(Of String) = New List(Of String)()
names.Add("Bill")
Dim name As String = names(0)

[C++]

IList&#139int;› ^numbers = gcnew List&#139int;›
numbers->Add(1);
int number = numbers[0];

IList&#139String; ^> ^names = gcnew List&#139String; ^>;
names->Add("Bill");
String ^name = names[0];

Divers tests ont montré que dans les modèles d'appel intensifs, les génériques améliorent les performances de 200 % en moyenne lors de l'utilisation de types de valeur, et de 100 % lors de l'utilisation de types de référence.

Cependant, les performances ne sont pas le principal avantage des génériques. Dans la plupart des applications réelles, les goulets d'étranglement tels que les E/S masquent les avantages des génériques en termes de performances. L'avantage le plus significatif des génériques est la sécurité du type. Avec les solutions basées objet, une non-correspondance de type est compilée, mais donne une erreur lors de l'exécution :

[C#]

IList numbers = new ArrayList();
numbers.Add(1);
string name = (string)numbers[0]; //Run-time error

[Visual Basic]

Public Class SomeClass
   ...
End Class

Dim numbers As IList = New ArrayList()
numbers.Add(1)
Dim obj As SomeClass = CType(numbers(0), SomeClass) 'Run-time error

[C++]

IList ^numbers = gcnew ArrayList;
numbers->Add(1);
String ^name = (String ^)numbers[0]; //Run-time error

Dans les bases de code volumineuses, ces erreurs sont difficiles à détecter et à résoudre. Avec les génériques, ce code ne serait jamais compilé :

[C#]

IList&#139int;› numbers = new List&#139int;›();
numbers.Add(1);
string name = numbers[0]; //Compile-time error

[Visual Basic]

Public Class SomeClass
   ...
End Class

Dim numbers As IList(Of Integer) = New List(Of Integer)()
numbers.Add(1)
Dim obj As SomeClass = numbers(0)'Compile-time error

[C++]

IList&#139int;› ^numbers = gcnew List&#139int;›
numbers->Add(1);
String ^name = numbers[0]; //Compile-time error

Pourquoi ne puis-je pas utiliser de structures de données propres au type plutôt que des génériques ?

Afin d'éviter le problème de la sécurité du type sans génériques, vous pouvez être tenté d'utiliser des interfaces et une structure de données propres au type, par exemple :

[C#]

public interface IIntegerList 
{
   int Add(int value);
   bool Contains(int value);
   int IndexOf(int value);
   void Insert(int index, int value);
   void Remove(int value);
   int this[int index]{ get; set; }
   //Additional members 
}

[Visual Basic]

Public Interface IIntegerList
    Function Add(ByVal value As Integer) As Integer
    Function Contains(ByVal value As Integer) As Boolean
    Function IndexOf(ByVal value As Integer) As Integer
    Sub Insert(ByVal index As Integer, ByVal value As Integer)
    Sub Remove(ByVal value As Integer)
    Property Item(ByVal index As Integer) As Integer
    'Additional members
End Interface

[C++]

public interface class IIntegerList 
{
   int Add(int value);
   bool Contains(int value);
   int IndexOf(int value);
   void Insert(int index, int value);
   property int default[]
   { 
      int get(int index); 
      void set(int index, int value); 
   }
   //Additional members 
};

Le problème de cette approche est que vous avez besoin d'une interface propre au type et d'une implémentation par type de données avec lequel vous souhaitez interagir, par exemple chaîne ou Client. En cas de défaillance de la gestion des données, vous devez résoudre le problème en autant d'endroits que de types, un processus peu pratique et comportant un risque important d'erreur. Avec les génériques, vous devez définir et implémenter votre logique une seule fois, tout en l'utilisant avec n'importe quel type souhaité.

Dans quels cas dois-je utiliser des génériques ?

Vous devez utiliser des génériques chaque fois que cela est possible. Cela signifie que, si une structure de données ou une classe d'utilitaire offre une version générique, vous devez utiliser la version générique et non les méthodes basées objet. La raison tient au fait que les génériques offrent des avantages significatifs, notamment la productivité, la sécurité du type et les performances, à un coût quasi nul pour vous. En général, les collections et les structures de données telles que les listes liées, les files d'attente, les arborescences binaires, etc. offrent la prise en charge des génériques, mais les génériques ne sont pas limités aux structures de données. Souvent, les classes d'utilitaire telles que les fabriques de classe ou les formateurs tirent également parti des génériques. Le ciblage croisé est le cas dans lequel vous ne devez pas tirer parti des génériques. Si vous développez votre code pour .NET 1.1 ou une version antérieure, vous ne devez utiliser aucune des nouvelles fonctionnalités de .NET 2.0, notamment les génériques. En C# 2.0, vous pouvez même indiquer au compilateur dans les paramètres du projet (sous Générer | Avancé) d'utiliser uniquement la syntaxe C# 1.0 (ISO-1).

Les génériques sont-ils covariants, contra-variants ou invariants ?

Les types génériques ne sont pas covariants. Cela signifie que vous ne pouvez pas remplacer un type générique avec un argument de type spécifique, par un autre type générique qui utilise un argument de type qui constitue le type de base du premier argument de type. Par exemple, l'instruction suivante n'est pas compilée :

[C#]

class MyBaseClass
{}
class MySubClass : MyBaseClass
{}
class MyClass&#139T;›
{}
//Will not compile 
MyClass&#139MyBaseClass;> obj = new MyClass&#139MySubClass;>();

[Visual Basic]

Public Class MyBaseClass
    ...
End Class
Public Class MySubClass
    Inherits MyBaseClass
    ...
End Class
Public Class SomeClass(Of T)
    ...
End Class
'Will not compile.
Dim obj As SomeClass(Of MyBaseClass) = New SomeClass(Of MySubClass)()

[C++]

ref class MyBaseClass
{};
ref class MySubClass : MyBaseClass
{};
generic &#139typename; T› where T : MyBaseClass
ref class MyClass
{};
//Will not compile 
MyClass&#139MySubClass; ^> ^obj = gcnew MyClass&#139MyBaseClass; ^>;

[C#]

Avec la même définition que dans l'exemple ci-dessus, il est également vrai que MyClass&#139MyBaseClass;> n'est pas le type de base de MyClass&#139MySubClass;>:

Debug.Assert(typeof(MyClass&#139MyBaseClass;>) != typeof(MyClass&#139MySubClass;>).BaseType);

[Visual Basic]

Avec la même définition que dans l'exemple ci-dessus, il est également vrai que SomeClass(Of MyBaseClass) n'est pas le type de base de SomeClass(Of MySubClass) :

Debug.Assert(GetType(SomeClass(Of MyBaseClass)) IsNot GetType(SomeClass(Of MySubClass)).BaseType)

[C++]

Avec la même définition que dans l'exemple ci-dessus, il est également vrai que MyClass&#139MyBaseClass;> n'est pas le type de base de MyClass&#139MySubClass;>:

Type ^baseType = typeid&#139MyClass;&#139MyBaseClass; ^> ^>;
Type ^subType  = typeid&#139MyClass;&#139MySubClass;  ^> ^>;
Debug::Assert(baseType != subType);

Cela ne serait pas le cas si les types génériques étaient contra-variants.

étant donné que les génériques ne sont pas covariants, lors du remplacement d'une méthode virtuelle qui renvoie un paramètre de type générique, vous ne pouvez pas fournir un sous-type de ce paramètre de type en tant que définition de la méthode de remplacement :

Par exemple, l'instruction suivante n'est pas compilée :

[C#]

class MyBaseClass&#139T;›
{
   public virtual T MyMethod()
   {...}
}
class MySubClass&#139T;,U> : MyBaseClass&#139T;› where T : U
{
   //Invalid definition: 
   public override U MyMethod()
   {...}
}

[Visual Basic]

Class MyBaseClass(Of T)
   Public Overridable Function MyMethod() As T
   ...
   End Function
End Class

Class MySubClass(Of T As U, U) 
   Inherits MyBaseClass(Of T)
End Class

[C++]

C++ n'autorise pas l'utilisation d'un type générique en tant que contrainte.

Cela étant, les contraintes sont covariantes. Par exemple, vous pouvez satisfaire une contrainte en utilisant un sous-type du type de contrainte :

[C#]

class MyBaseClass
{}
class MySubClass : MyBaseClass
{}
class MyClass&#139T;› where T : MyBaseClass
{}

MyClass&#139MySubClass;> obj = new MyClass&#139MySubClass;>();

[Visual Basic]

Class MyBaseClass
    ...
End Class
Class MySubClass
    Inherits MyBaseClass
    ...
End Class
Class SomeClass(Of T As MyBaseClass)
    ...
End Class
Dim obj As New SomeClass(Of MySubClass)()

[C++]

ref class MyBaseClass
{};
ref class MySubClass : MyBaseClass
{};
generic &#139typename; T› where T : MyBaseClass
ref class MyClass 
{};

MyClass&#139MySubClass; ^> ^obj = gcnew MyClass&#139MySubClass; ^>;

Vous pouvez même limiter davantage les contraintes de cette façon :

[C#]

class BaseClass&#139T;›  where T : IMyInterface
{}
interface IMyOtherInterface : IMyInterface
{}
 
class SubClass&#139T;› : BaseClass&#139T;› where T : IMyOtherInterface 
{}

[Visual Basic]

Class BaseClass(Of T As IMyInterface)
    ...
End Class
Interface IMyOtherInterface
    Inherits IMyInterface
    ...
End Interface
Class SubClass(Of T As IMyOtherInterface)
    Inherits BaseClass(Of T)
    ...
End Class

[C++]

interface class IMyInterface 
{};
generic &#139typename; T› where T : IMyInterface
ref class BaseClass  
{};
interface class IMyOtherInterface : IMyInterface
{};
generic &#139typename; T› where T : IMyOtherInterface
ref class SubClass : BaseClass&#139T;›
{};

Enfin, les génériques sont invariants, car il n'y a pas de relation entre deux types génériques avec différents arguments de type, même si ces arguments de type présentent une relation is-as, par exemple List&#139int;› n'a rien à voir avec List&#139object;>, même si un type int est un objet.

Qu'est-ce qui peut définir des paramètres de type générique ? Quels types peuvent être génériques ?

Les classes, interfaces, structures et délégués peuvent tous être des types génériques. Voici quelques exemples du .NET Framework :

[C#]

public interface IEnumerator&#139T;› : IEnumerator,IDisposable
{
   T Current{get;}
}

public class List&#139T;› : IList&#139T;› //More interfaces 
{
   public void Add(T item);
   public bool Remove(T item);
   public T this[int index]{get;set;}
   //More members 
}

public struct KeyValuePair&#139K;,V>
{
   public KeyValuePair(K key,V value);   
   public K Key;
   public V Value;
}

public delegate void EventHandler&#139E;>(object sender,E e) where E : EventArgs;

[Visual Basic]

Public Interface IEnumerator(Of T)
    Inherits IDisposable , IEnumerator
    ReadOnly Property current As T
End Interface

Public Class list(Of T)
    Inherits IList(Of T)'More interfaces

    Public Sub Add(ByVal item As T)
    Public Function Remove(ByVal item As T) As Boolean
    ' More members
End Class

Public Struct KeyValuePair(Of K, V)
    Public Sub New(key As K, value As V)
    Public Key As K
    Public Value As V
End Structure
Public Delegate Sub EventHandler(Of E As EventArgs) _
  (ByVal sender As Object, ByVal e As E)

[C++]

generic &#139typename; T›
public interface class IEnumerator : IEnumerator,IDisposable
{
   property T Current { T get(); }
};

generic &#139typename; T›
public ref class List : IList&#139T;› //More interfaces 
{
public: 
   void Add(T item);
   bool Remove(T item);
   property T default[] { T get(int index); void set(int index, T value); }
   //More memebers 
};
generic &#139typename; K, typename V>
public ref struct KeyValuePair
{
public: 
   KeyValuePair(K key, V value);   
   K Key;
   V Value;
};

generic &#139typename; T› where T: EventArgs
public delegate void EventHandler(Object ^sender, T e);

En outre, les méthodes statiques et les méthodes d'instance peuvent reposer sur des paramètres de type générique, indépendamment des types qui les contiennent :

[C#]

public sealed class Activator : _Activator
{
   public static T CreateInstance&#139T;›();
   //Additional memebrs 
}

[Visual Basic]

Public NotInheritable Class Activator
      Implements _Activator

    Public Shared Function CreateInstance(Of T)() As T
    ' Additional members.
End Class

[C++]

public ref class Activator sealed : _Activator
{
   public: generic &#139typename; T›
           static T CreateInstance();
   //Additional members 
};

à l'inverse, les énumérations ne peuvent pas définir de paramètres de type ; il en va de même des attributs.

Des méthodes peuvent-elles définir des paramètres de type générique ? Comment appeler ces méthodes ?

Oui. Les méthodes d'instance et les méthodes statiques peuvent définir des paramètres de type générique, indépendamment de la classe qui les contient. Par exemple :

[C#]

public class MyClass
{
   public void MyInstanceMethod&#139T;›(T t)
   {...}
   public static void MyStaticMethod&#139T;›(T t)
   {...}
}

[Visual Basic]

Public Class SomeClass
    Public Sub MyInstanceMethod(Of T)(ByVal value As T)
        ...
    End Sub
    Public Shared Sub MySharedMethod(Of T)(ByVal value As T)
        ...
    End Sub
End Class

[C++]

public ref class MyClass
{
public:      
   generic &#139typename; T›
   void MyInstanceMethod (T t)
   {...}
   generic &#139typename; T›
   static void MyStaticMethod (T t)
   {...}
};

L'avantage d'une méthode qui définit des paramètres de type générique est que vous pouvez appeler la méthode qui transmet chaque fois des types de paramètre différents, sans jamais surcharger la méthode. Lorsque vous appelez une méthode qui définit des paramètres de type générique, vous devez fournir les arguments de type sur le site appelant :

[C#]

MyClass obj = new MyClass();
obj.MyInstanceMethod&#139int;›(3);
obj.MyInstanceMethod&#139string;›("Hello");

MyClass.MyStaticMethod&#139int;›(3);
MyClass.MyStaticMethod&#139string;›("Hello");

[Visual Basic]

Dim obj As New SomeClass()
obj.MyInstanceMethod(Of Integer)(3)
obj.MyInstanceMethod(Of String)("Hello")
SomeClass.MySharedMethod(Of Integer)(3)
SomeClass.MySharedMethod(Of String)("Hello")

[C++]

MyClass ^obj = gcnew MyClass;
obj->MyInstanceMethod&#139int;›(3);
obj->MyInstanceMethod&#139String; ^>("Hello");

MyClass::MyStaticMethod&#139int;›(3);
MyClass::MyStaticMethod&#139String; ^>("Hello");

Si l'inférence de type est disponible, vous pouvez omettre les arguments de type sur le site d'appel :

[C#]

MyClass obj = new MyClass();
obj.MyInstanceMethod(3);
obj.MyInstanceMethod("Hello");

MyClass.MyStaticMethod(3);
MyClass.MyStaticMethod("Hello");

[Visual Basic]

Dim obj As New SomeClass()
obj.MyInstanceMethod(3)
obj.MyInstanceMethod("Hello")
SomeClass.MySharedMethod(3)
SomeClass.MySharedMethod("Hello")

[C++]

MyClass ^obj = gcnew MyClass;
obj->MyInstanceMethod(3);
obj->MyInstanceMethod(gcnew String("Hello"));

MyClass::MyStaticMethod(3);
MyClass::MyStaticMethod(gcnew String("Hello"));

Peut-on dériver une classe à partir d'un paramètre de type générique ?

Vous ne pouvez pas définir une classe dérivée de son propre paramètre de type générique :

[C#]

public class MyClass&#139T;› : T //Does not compile 
{...}

[Visual Basic]

Public Class SomeClass(Of T)
    Inherits T ' Does not compile
    ...
End Class

[C++]

generic &#139typename; T›
public ref class MyClass : T //Does not compile 
{...};

Qu'est-ce qu'une interférence de type générique ?

L'inférence de type générique est la capacité du compilateur à déterminer les arguments de type à utiliser avec une méthode générique, sans que le développeur n'ait à le spécifier explicitement. Par exemple, examinez la définition suivante de méthodes génériques :

[C#]

public class MyClass
{
   public void MyInstanceMethod&#139T;›(T t)
   {...}
   public static void MyStaticMethod&#139T;›(T t)
   {...}
}

[Visual Basic]

Public Class SomeClass
    Public Sub MyInstanceMethod(Of T)(ByVal value As T)
        ...
    End Sub
    Public Shared Sub MySharedMethod(Of T)(ByVal value As T)
        ...
    End Sub
End Class

[C++]

public class MyClass
{
public: 
   generic &#139typename; T›      
   void MyInstanceMethod(T t) {...}
   generic &#139typename; T›   
   static void MyStaticMethod (T t) {...}
};

Lors de l'appel de ces méthodes, vous pouvez omettre de spécifier les arguments de type pour les méthodes d'instance et les méthodes statiques :

[C#]

MyClass obj = new MyClass();
obj.MyInstanceMethod(3); //Compiler infers T as int
obj.MyInstanceMethod("Hello");//Compiler infers T as string

MyClass.MyStaticMethod(3); //Compiler infers T as int
MyClass.MyStaticMethod("Hello");//Compiler infers T as string

[Visual Basic]

Dim obj As New SomeClass()
obj.MyInstanceMethod(3) ' Compiler infers T as int
obj.MyInstanceMethod("Hello") ' Compiler infers T as String
SomeClass.MySharedMethod(3) ' Compiler infers T as Integer
SomeClass.MySharedMethod("Hello") ' Compiler infers T as string

[C++]

MyClass ^obj = gcnew MyClass;
obj->MyInstanceMethod(3); //Compiler infers T as int
MyClass::MyStaticMethod(3); //Compiler infers T as int

Notez que l'inférence de type est possible uniquement lorsque la méthode accepte un argument des arguments de type inférés. Par exemple, dans la méthode CreateInstance&#139T;›() de la classe Activator, définie comme suit :

[C#]

public sealed class Activator : _Activator
{
   public static T CreateInstance&#139T;›();
   //Additional memebrs 
}

[Visual Basic]

Public NotInheritable Class Activator
      Implements _Activator

    Public Shared Function CreateInstance(Of T)() As T
    ' Additional members.
End Class

[C++]

public ref class Activator sealed : _Activator
{
public: 
   generic &#139typename; T›
   static T CreateInstance ();
   //Additional members 
};

l'inférence de type n'est pas possible et vous devez spécifier les arguments de type sur le site d'appel :

[C#]

class MyClass
{...} 
MyClass obj = Activator.CreateInstance&#139MyClass;>(); 

[Visual Basic]

Public Class SomeClass
    ...
End Class
Dim obj As SomeClass = activator.createInstance(Of SomeClass)()

[C++]

ref class MyClass
{...}; 
MyClass ^obj = Activator::CreateInstance&#139MyClass; ^>(); 

Notez également que vous ne pouvez pas utiliser l'inférence de type au niveau du type, mais uniquement au niveau de la méthode. Dans l'exemple suivant, vous devez quand même fournir l'argument de type T, même si la méthode accepte un paramètre T :

[C#]

public class MyClass&#139T;›
{
   public static void MyStaticMethod&#139U;>(T t,U u)
   {...}
}
MyClass&#139int;›.MyStaticMethod(3,"Hello");//No type inference for the integer 

[Visual Basic]

Public Class SomeClass(Of T)
    Public Shared sub MySharedMethod(Of U)(ByVal item As T, ByVal uu As U)
        ...
    End Sub
End Class
SomeClass(Of Integer).MySharedMethod(3, "Hello") 
'No type inference for the integer

[C++]

generic &#139typename; T›
public ref class MyClass
{
   public: generic &#139typename; U> static void MyStaticMethod (T t,U u)
   {...}
};
MyClass&#139int;›::MyStaticMethod(3, 22.7);//No type inference for the integer 

Que sont les contraintes ?

Les contraintes permettent d'ajouter des informations contextuelles supplémentaires aux paramètres de type des types génériques. Les contraintes limitent la plage des types pouvant être utilisés comme arguments de type, mais elles ajoutent simultanément des informations sur ces paramètres de type. Les contraintes garantissent que les arguments de type spécifiés par le code client sont compatibles avec les paramètres de type générique utilisés par le type générique proprement dit. Cela signifie que les contraintes empêchent le client de spécifier en tant qu'arguments de type des types qui n'offrent pas les méthodes, propriétés ou membres des paramètres de type générique sur lesquels est basé le type générique.

Après l'application d'une contrainte, IntelliSense reflète les contraintes lors de l'utilisation du paramètre de type générique, par exemple la suggestion de méthodes ou de membres à partir du type de base.

Il existe trois types de contraintes :

La contrainte de dérivation indique au compilateur que les paramètres de type générique sont issus d'un type de base tel qu'une interface ou une classe de base particulière. Par exemple, dans l'exemple suivant, la liste liée applique une contrainte de dérivation de IComparable&#139T;› sur son paramètre de type générique. Cela est nécessaire afin que vous puissiez implémenter une fonctionnalité de recherche, de tri ou d'indexation sur la liste :

[C#]

class Node&#139K;,T›
{
   public K Key;
   public T Item;
   public Node&#139K;,T› NextNode;
} 

public class LinkedList&#139K;,T› where K : IComparable&#139K;>
{
   Node&#139K;,T› m_Head;

   public T this[K key]
   {
      get
      {
         Node&#139K;,T› current = m_Head;
         while(current.NextNode != null)
         {
            if(current.Key.CompareTo(key) == 0)
               break;
            else      
               current = current.NextNode;
         }
         return current.Item; 
      }
   }
   //Rest of the implementation 
}

[Visual Basic]

Class Node(Of K, T)
    Public Key As K
    Public Item As T
    Public NextNode As Node(Of K, T)
End Class

Public Class LinkedList(Of K As IComparable(Of K), T)
    Dim m_Head As Node(Of K, T)
    Public ReadOnly Property Item(ByVal key As K) As T
        Get
            Dim current As Node(Of K, T) = m_Head
            While current.NextNode IsNot Nothing
                If (current.Key.CompareTo(key) = 0) Then
                    Exit While
                Else
                    current = current.NextNode
                End If
            End While
            Return current.item
        End Get
    End Property
    ' Rest of the implementation
End Class

[C++]

generic &#139typename; K, typename T›
ref class Node
{
public: K Key;
        T Item;
        Node&#139K;,T› ^NextNode;
};
generic &#139typename; K, typename T› where K : IComparable&#139K;>
public ref class LinkedList
{
public:
   Node&#139K;,T› ^m_Head;
public: 
   property T default[]
   {
      T get(K key)
      {
         Node&#139K;,T› ^current = m_Head;
         while(current->NextNode)
         {
            if(current->Key->CompareTo(key)==0)
               break;
            else      
               current = current->NextNode;
         }
         return current->Item; 
      }
   }
   //Rest of the implementation 
};

Vous pouvez fournir des contraintes pour chaque paramètre de type générique déclaré par votre classe, par exemple :

[C#]

public class LinkedList&#139K;,T› where K : IComparable&#139K;>
                             where T : ICloneable 

[Visual Basic]

Public Class LinkedList(Of K As IComparable(Of K), T As ICloneable)
    ...
End Class

[C++]

generic &#139typename; K, typename T› where K : IComparable&#139K;>
                      where T : ICloneable
public ref class LinkedList  
{ ... };                         

Vous pouvez définir une contrainte de classe de base, c'est-à-dire stipuler que le paramètre de type générique est dérivé d'une classe de base particulière :

[C#]

public class MyBaseClass
{...}
public class MyClass&#139T;› where T : MyBaseClass
{...}

[Visual Basic]

Public Class MyBaseClass
    ...
End Class
Public Class SomeClass(Of T As MyBaseClass)
    ...
End Class

[C++]

public ref class MyBaseClass
{...};
generic &#139typename; T› where T : MyBaseClass
public ref class MyClass
{...};

En revanche, vous pouvez utiliser une seule classe de base dans une contrainte, car ni C#, ni Visual Basic, ni C++ géré ne prend en charge l'héritage multiple de l'implémentation. à l'évidence, la classe de base de contrainte ne peut pas être une classe sealed, condition imposée par le compilateur. En outre, vous ne pouvez pas contraindre System.Delegate ou System.Array en tant que classe de base.

Vous pouvez restreindre à la fois une classe de base et une ou plusieurs interfaces, mais la classe de base doit apparaître en premier dans la liste des contraintes de dérivation :

[C#]

public class LinkedList&#139K;,T› where K : MyBaseKey,IComparable&#139K;>
{...}

[Visual Basic]

Public Class LinkedList(Of K As {MyBaseKey, IComparable(Of K)}, T)
    ...
End Class

[C++]

generic &#139typename; K, typename T› where K : MyBaseKey, IComparable&#139K;>
public class LinkedList
{...};

La contrainte de constructeur indique au compilateur que le paramètre de type générique expose un constructeur public par défaut (un constructeur public sans paramètres). Par exemple :

[C#]

class Node&#139K;,T› where K : new()
               where T : new()
{
   public K Key;
   public T Item;
   public Node&#139K;,T› NextNode;

   public Node()
   {
      Key      = new K(); //Compiles because of the constraint
      Item     = new T(); //Compiles because of the constraint
      NextNode = null;
   }
   //Rest of the implementation    
}

[Visual Basic]

Class Node(Of K As New, T As New)
    Public Key  As K
    Public Item As T
    Public NextNode As Node(Of K, T)
    Public Sub New()
        Key  = New K()' Compiles because of the constraint
        Item = New T()' Compiles because of the constraint
        NextNode = Nothing
    End Sub
    ' Rest of the implementation.
End Class

[C++]

Vous pouvez combiner la contrainte de constructeur par défaut avec les contraintes de dérivation, sous réserve que la contrainte de constructeur par défaut apparaisse en dernier dans la liste de contrainte :

[C#]

public class LinkedList&#139K;,T› where K : IComparable&#139K;>,new()
                             where T : new() 
{...}

[Visual Basic]

Public Class LinkedList(Of K As {IComparable(Of K), New}, T As New)
    ...
End Class

La contrainte de référence et de type de valeur est utilisée pour contraindre le paramètre de type générique en tant que type de valeur ou de référence. Par exemple, vous pouvez contraindre un paramètre de type générique en tant que type de valeur (par exemple int, bool ou enum, ou une structure quelconque) :

[C#]

public class MyClass&#139T;› where T : struct 

{...}

[Visual Basic]

Public Class SomeClass(Of T As Structure)
    ...
End Class

De la même façon, vous pouvez contraindre un paramètre de type générique en tant que type de référence (classe) :

[C#]

public class MyClass&#139T;› where T : class 

{...}

[Visual Basic]

Public Class SomeClass(Of T As Class)
    ...
End Class

La contrainte de type valeur/référence ne peut pas être utilisée avec une contrainte de classe de base, mais elle peut être associée à n'importe quelle autre contrainte. Lorsqu'elle est utilisée, la contrainte de type valeur/référence doit apparaître en premier dans la liste des contraintes.

Il est important de noter que si les contraintes sont facultatives, elles sont souvent essentielles lors du développement d'un type générique. Sans contraintes, le compilateur suit l'approche plus conservatrice et sécurisée et autorise uniquement l'accès aux fonctionnalités de niveau objet dans vos paramètres de type générique. Les contraintes font partie des métadonnées de type générique, de sorte que le compilateur côté client peut en tirer parti également. Le compilateur côté client autorise uniquement le développeur client à utiliser des types conformes aux contraintes, appliquant ainsi la sécurité du type.

Avec quoi ne puis-je pas utiliser de contraintes ?

Vous pouvez uniquement placer une contrainte de dérivation sur un paramètre de type (qu'il s'agisse d'une dérivation d'interface ou d'une dérivation de classe de base unique). En C# et Visual Basic, vous pouvez également utiliser une contrainte de constructeur par défaut et une contrainte de type valeur ou référence. Si tout le reste est implicitement interdit, il convient de mentionner les cas spécifiques qui ne sont pas autorisés :

  • Vous ne pouvez pas contraindre un type générique à présenter une structure paramétrée spécifique.

  • Vous ne pouvez pas contraindre un type générique à être dérivé d'une classe sealed.

  • Vous ne pouvez pas contraindre un type générique à être dérivé d'une classe statique.

  • Vous ne pouvez pas contraindre un type générique public à être dérivé d'un autre type interne.

  • Vous ne pouvez pas contraindre un type générique à comporter une méthode spécifique, fût-elle une méthode statique ou une méthode d'instance.

  • Vous ne pouvez pas contraindre un type générique à comporter un événement public spécifique.

  • Vous ne pouvez pas contraindre un paramètre de type générique à être dérivé de System.Delegate ou System.Array.

  • Vous ne pouvez pas contraindre un paramètre de type générique à être sérialisable.

  • Vous ne pouvez pas contraindre un paramètre de type générique à être visible par COM.

  • Vous ne pouvez pas contraindre un paramètre de type générique à comporter un attribut particulier.

  • Vous ne pouvez pas contraindre un paramètre de type générique à prendre en charge un opérateur spécifique. Il n'existe donc aucun moyen de compiler le code suivant :

[C#]

public class Calculator&#139T;›
{
   public T Add(T argument1,T argument2)
   {
      return argument1 + argument2; //Does not compile 
   }
   //Rest of the methods 
}

[Visual Basic]

Public Class calculator(Of T)
    Public Function add(ByVal argument1 As T, ByVal argument2 As T) As T
        Return argument1 + argument2
        ' The preceding statement does not compile.
    End Function
    ' Rest of the methods.
End Class

[C++]

generic &#139typename; T›
public ref class Calculator
{
public: T Add(T argument1,T argument2)
   {
      return argument1 + argument2; //Does not compile 
   }
   //Rest of the methods 
};

Pourquoi ne puis-je pas utiliser de classes Enum, Struct ou Sealed en tant que contraintes génériques ?

Vous ne pouvez pas contraindre un paramètre de type générique à être dérivé d'un type non dérivable. Par exemple, l'instruction suivante n'est pas compilée :

[C#]

public sealed class MySealedClass
{...}
public class MyClass&#139T;› where T : MySealedClass //Does not compile 
{...}

[Visual Basic]

Public NotInheritable Class MySealedClass
    ...
End Class
Public Class SomeClass(Of T As MySealedClass
' The preceding statement does not compile.
    ...
End Class

[C++]

public ref class MySealedClass sealed
{...};
generic &#139typename; T› where T : MySealedClass
public ref class MyClass //Does not compile 
{...};

La raison est simple : les seuls arguments de type pouvant satisfaire à la contrainte ci-dessus sont le type MySealedClass proprement dit, ce qui rend l'utilisation des génériques redondante. Pour cette raison, tous les autres types non dérivables, tels que les structures et les énumérations, ne sont pas autorisés dans les contraintes.

Le code qui utilise les génériques est-il plus rapide que le code qui ne les utilise pas ?

La réponse dépend de la façon dont le code non générique est écrit. Si le code utilise des objets tels que les conteneurs amorphes pour stocker les éléments, les divers tests ont montré que dans les modèles d'appel intensif, les génériques offrent une amélioration moyenne des performances de 100 % (c'est-à-dire trois fois plus rapide) lors de l'utilisation de types de valeur, et de 50 % lors de l'utilisation de types de référence.

Si le code non générique utilise des structures de données propres au type, les génériques n'offrent aucune amélioration des performances. Cependant, un tel code est par nature très fragile. L'écriture d'une structure de données propre au type est une tâche fastidieuse, répétitive et présentant un risque élevé d'erreur. Lorsque vous résolvez un problème dans la structure de données, vous devez le résoudre non pas dans un seul endroit, mais dans autant d'endroits qu'il y a de doublons propres au type d'une structure de données essentiellement identique.

Une application qui utilise les génériques est-elle plus rapide qu'une application qui ne les utilise pas ?

Cela dépend bien sûr de l'application, mais de façon générale, les goulets d'étranglement tels que les E/S masquent les avantages des génériques en termes de performances. L'avantage réel des génériques n'est pas l'amélioration des performances, mais plutôt la sécurité du type et la productivité.

En quoi les génériques sont-ils similaires aux modèles Visual C++ classiques ?

Les génériques présentent un concept similaire aux modèles C++ classiques : les deux autorisent les structures de données ou les classes d'utilitaire à différer au niveau du client le choix des types réels à utiliser, et les deux offrent des avantages en termes de productivité et de sécurité du type.

En quoi les génériques sont-ils différents des modèles Visual C++ classiques ?

Il existe deux différences essentielles : le modèle de programmation et l'implémentation sous-jacente. Dans le modèle de programmation, les génériques .NET peuvent offrir une sécurité améliorée par rapport aux modèles Visual C++ classiques. Les génériques .NET ont la notion de contraintes, ce qui renforce la sécurité du type. Par ailleurs, les génériques .NET offrent un modèle de programmation plus restrictif ; il existe des choses que les génériques ne peuvent pas faire, par exemple l'utilisation d'opérateurs, car il n'est pas possible de contraindre un paramètre de type afin de prendre en charge un opérateur. Cela n'est pas le cas des modèles Visual C++ classiques, dans lesquels vous pouvez appliquer n'importe quel opérateur souhaité sur les paramètres de type. Lors de la compilation, le compilateur Visual C++ classique remplace tous les paramètres de type du modèle par votre type spécifié, et les éventuelles incompatibilités sont généralement découvertes à ce moment-là.

Les modèles et les génériques peuvent tous deux entraîner une augmentation du code, mais les deux présentent des mécanismes permettant de limiter cette augmentation. L'instanciation d'un modèle avec un ensemble spécifique de types instancie uniquement les méthodes réellement utilisées ; toutes les méthodes qui donnent un code identique sont automatiquement fusionnées par le compilateur, ce qui évite toute duplication inutile. L'instanciation d'un générique avec un ensemble spécifique de types instancie toutes ses méthodes, mais une seule fois pour tous les arguments de type de référence ; l'augmentation du code provient uniquement des types de valeur, car le CLR instancie un générique séparément une fois pour chaque argument de type de valeur. Enfin, les génériques .NET vous permettent de fournir des fichiers binaires, tandis que les modèles C++ nécessitent le partage de code avec le client.

Quelle est la différence entre l'utilisation de génériques et l'utilisation d'interfaces (ou de classes abstraites) ?

Les interfaces et les génériques ont des objectifs différents. Les interfaces concernent la définition d'un contrat entre un consommateur de service et un fournisseur de service. Tant que le consommateur programme strictement par rapport à l'interface (et non par rapport à une implémentation particulière), il peut utiliser n'importe quel autre fournisseur de service prenant en charge la même interface. Cela permet de changer de fournisseur de service sans affecter le code du client (ou avec un impact minimal). L'interface permet également au même fournisseur de service d'offrir des services à différents clients. Les interfaces sont la base de l'ingénierie logicielle moderne et sont utilisées de manière intensive dans les technologies passées et futures, de COM à .NET en passant par Indigo et SOA.

Les génériques concernent la définition et l'implémentation d'un service sans validation des types réellement utilisés. Par conséquent, les interfaces et les génériques ne sont pas mutuellement exclusifs. Loin de là, ils sont même complémentaires l'un de l'autre. Vous pouvez et devez combiner les interfaces et les génériques.

Par exemple, l'interface ILinkedList&#139T;› définie comme suit :

[C#]

public interface ILinkedList&#139T;›
{
   void AddHead(T item);
   void RemoveHead(T item);
   void RemoveAll();
}

[Visual Basic]

Public Interface ILinkedList(Of T)
    Sub AddHead(ByVal item As T)
    Sub RemoveHead(ByVal item As T)
    Sub RemoveAll()
End Interface

[C++]

generic &#139typename; T›
public interface class ILinkedList
{
   void AddHead(T item);
   void RemoveHead(T item);
   void RemoveAll();
};

Peut être implémentée par n'importe quelle liste liée :

[C#]

public class LinkedList&#139T;› : ILinkedList&#139T;›
{...}

public class MyOtherLinkedList&#139T;› : ILinkedList&#139T;›
{...}

[Visual Basic]

Public Class LinkedList(Of T)
    Implements ILinkedList(Of T)
    ...
End Class
Public Class MyOtherinkedList(Of T)
    Implements ILinkedList(Of T)
    ...
End Class

[C++]

generic &#139typename; T›
public ref class LinkedList : ILinkedList&#139T;›
{...};

generic &#139typename; T›
public ref class MyOtherLinkedList : ILinkedList&#139T;›
{...};

Vous pouvez à présent programmer par rapport à ILinkedList&#139T;›, en utilisant des implémentations différentes et des types d'argument différents :

[C#]

ILinkedList&#139int;› numbers  = new LinkedList&#139int;›();
ILinkedList&#139string;> names = new LinkedList&#139string;>();

ILinkedList&#139int;› moreNumbers = new MyOtherLinkedList&#139int;›();

[Visual Basic]

Dim numbers As ILinkedList(Of Integer) = New LinkedList(Of Integer)()
Dim names As ILinkedList(Of String) = New LinkedList(Of String)()
Dim moreNumbers As ILinkedList(Of Integer) = New MyOtherLinkedList(Of Integer)()

[C++]

ILinkedList&#139int;› ^numbers    = gcnew LinkedList&#139int;›
ILinkedList&#139String; ^> ^names = gcnew LinkedList&#139String; ^>;

ILinkedList&#139int;› ^moreNumbers = gcnew MyOtherLinkedList&#139int;›();

Comment les génériques sont-ils implémentés ?

Les génériques bénéficient d'une prise en charge native dans IL et dans le CLR proprement dit. Lorsque vous compilez le code générique côté serveur, le compilateur le compile en IL, comme n'importe quel autre type. Cependant, IL contient uniquement des paramètres ou des espaces réservés pour les types spécifiques réels. En outre, les métadonnées du serveur générique contiennent des informations génériques telles que des contraintes.

Le compilateur côté client utilise ces métadonnées génériques pour prendre en charge la sécurité du type. Lorsque le client fournit des arguments de type, le compilateur du client remplace le paramètre de type générique par le type spécifié dans les métadonnées du serveur. Le compilateur du client dispose ainsi d'une définition du serveur propre au type, comme si les génériques n'avaient jamais été impliqués. Lors de l'exécution, le code machine réellement généré varie selon que les types spécifiés sont de type valeur ou référence. Si le client spécifie un type valeur, le compilateur JIT remplace les paramètres de type générique dans le IL par le type de valeur spécifique, puis le compile en code natif. Cependant, le compilateur JIT effectue le suivi du code serveur propre au type déjà généré. Si le compilateur JIT est invité à compiler le serveur générique avec un type de valeur déjà compilé en code machine, il renvoie simplement une référence à ce code serveur. étant donné que le compilateur JIT utilise le même code serveur propre au type de valeur par la suite, le code n'augmente pas.

Si le client spécifie un type de référence, le compilateur JIT remplace les paramètres génériques de l'IL serveur par l'objet, puis le compile en code natif. Ce code est utilisé dans les demandes suivantes d'un type de référence, plutôt qu'un paramètre de type générique. Notez que de cette façon, le compilateur JIT réutilise uniquement le code réel. Les instances sont toujours allouées en fonction de leur taille sur le tas géré, et il n'y a pas de conversion.

Pourquoi ne puis-je pas utiliser d'opérateurs sur les paramètres de type générique naked ?

La raison est simple : étant donné qu'il n'est pas possible de contraindre un paramètre de type générique à prendre en charge un opérateur, le compilateur ne peut pas savoir si le type spécifié par le client du type générique prendra en charge l'opérateur.

Examinez par exemple le code suivant :

[C#]

class Node&#139K;,T›
{
   public K Key;
   public T Item;
   public Node&#139K;,T› NextNode;
} 

public class LinkedList&#139K;,T› 
{
   Node&#139K;,T› m_Head;

   public T this[K key]
   {
      get
      {
         Node&#139K;,T› current = m_Head;
         while(current.NextNode != null)
         {
            if(current.Key == key)) //Does not compile
               break;
            else      
               current = current.NextNode;
         }
         return current.Item; 
      }
   }
   //Rest of the implementation 
}

[Visual Basic]

Class Node(Of K, T)
    Public Key As K
    Public Item As T
    Public NextNode As Node(Of K, T)
End Class

&#139DefaultMember;("Item")> _ 
Public Class LinkedList(Of K, T)
    
    Dim m_Head As Node(Of K, T)

    Public ReadOnly Property Item(ByVal key As K) As T
        Get
            Dim current As Node(Of K, T) = m_Head
            While current.NextNode IsNot Nothing
                If current.key = key Then
            ' The preceding statement does not compile.
                    Exit While
                Else
                    current = current.NextNode
                End If
            End While
            Return current.item
        End Get
    End Property
    ' Rest of the implementation
End Class

[C++]

generic &#139typename; K, typename T›
ref class Node
{
public: K Key;
        T Item;
        Node&#139K;,T› ^NextNode;
};

generic &#139typename; K, typename T›
public ref class LinkedList 
{
public:
   Node&#139K;,T› ^m_Head;
public:
   property T default[]
   {
      T get(K key)
      {
         Node&#139K;,T› ^current = m_Head;
         while(current->NextNode)
         {
            if(current->Key == key)) //Does not compile
               break;
            else      
               current = current->NextNode;
         }
         return current->Item; 
      }
   }
   //Rest of the implementation 
};

Le compilateur refuse de compiler cette ligne :

[C#]

if(current.Key == key))

[Visual Basic]

If current.key = key Then

[C++]

if(current->Key == key))

car il n'est pas possible de savoir si le type qui sera spécifié par le consommateur prendra en charge l'opérateur ==.

Pourquoi puis-je utiliser des opérateurs sur les paramètres de type générique ?

Vous pouvez utiliser un opérateur (ou n'importe quelle méthode propre au type, dans le cas présent) sur des paramètres de type générique si le paramètre de type générique est contraint d'être un type prenant en charge cet opérateur. Par exemple :

[C#]

class MyOtherClass
{
   public static MyOtherClass operator+(MyOtherClass lhs,MyOtherClass rhs)
   {
      MyOtherClass product = new MyOtherClass();
      product.m_Number = lhs.m_Number + rhs.m_Number;
      return product;
   }
   int m_Number;
   //Rest of the class 
}

class MyClass&#139T;› where T : MyOtherClass
{
   MyOtherClass Sum(T t1,T t2)
   {
      return t1 + t2;
   }
}

[Visual Basic]

Class MyOtherClass
   Public Shared Function op_Addition(ByVal lhs As MyOtherClass, 
                                      ByVal rhs As MyOtherClass) As MyOtherClass
      Dim product As New MyOtherClass
      product.m_Number =  lhs.m_Number + rhs.m_Number
      return product
   End Function   
   Private m_Number As Integer
End Class

Class SomeClass(Of T As MyOtherClass)
   Private Function Sum(ByVal t1 As T, ByVal t2 As T) As MyOtherClass
      Return (t1 + t2)
   End Function
End Class

Puis-je utiliser des attributs génériques ?

Vous ne pouvez pas définir d'attributs génériques :

[C#]

//This is not possible:
class MyAttribute&#139T;›: Attribute
{...}

[Visual Basic]

' The following declaration is not possible.
Public Class MyAttribute(Of T)
    Inherits Attribute
    ...
End Class

[C++]

//This is not possible:
generic &#139typename; T›
ref class MyAttribute: Attribute
{...};

Cependant, rien ne vous empêche d'utiliser des génériques en interne, à l'intérieur de l'implémentation de l'attribut.

Les génériques sont-ils compatibles CLS ?

Oui. Avec la version .NET 2.0, les génériques feront partie du CLS.

à propos de l'auteur

Juval Lowy est architecte logiciel et responsable de IDesign, spécialisé dans le consulting sur l'architecture .NET et la formation avancée à .NET. Juval est le Directeur régional de Microsoft pour la Silicon Valley et travaille avec Microsoft afin de favoriser l'adoption de .NET. Son dernier ouvrage est Programming .NET Components 2nd Edition (Programmation de composants NET, éditions O'Reilly, 2005). Juval participe aux tests de conception internes de Microsoft pour les versions futures de .NET. Juval a publié de nombreux articles (en anglais), sur presque tous les aspects du développement .NET, et intervient fréquemment lors des conférences sur le développement. Microsoft a attribué à Juval le titre de "légende logicielle", comme l'un des meilleurs experts .NET au monde.

Afficher: