FAQ sur les génériques : Principes de base

 

Juval Lowy

Octobre 2005

S’applique à :
   Types génériques

Résumé: Passez en revue les questions fréquemment posées sur les types génériques et leurs différentes utilisations. (51 pages imprimées)

Contenu

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 Type-Specific structures de données au lieu de génériques ?
Quand dois-je utiliser des génériques ?
Les génériques sont-ils covariants, contra-variant ou invariants ?
Qu’est-ce qui peut définir des paramètres de type générique ? Quels types peuvent être génériques ?
Les méthodes peuvent-elles définir des paramètres de type générique ? Comment appeler de telles méthodes ?
Puis-je dériver d’un paramètre de type générique ?
Qu’est-ce qu’une inférence de type générique ?
Que sont les contraintes ?
Avec quoi ne puis-je pas utiliser les contraintes ?
Pourquoi ne puis-je pas utiliser des énumérations, des structs ou des classes sealed comme contraintes génériques ?
Le code qui utilise des génériques est-il plus rapide que le code qui ne le fait pas ?
Une application qui utilise des génériques est-elle plus rapide qu’une application qui ne le fait 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 des opérateurs sur des paramètres de type générique nu ?
Quand puis-je utiliser des opérateurs sur des paramètres de type générique ?
Puis-je utiliser des attributs génériques ?
Les génériques sont-ils conformes 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<K,T>, défini comme suit :

[C#]

public class LinkedList<K,T>
{...}

[Visual Basic]

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

[C++]

generic <typename 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 l’élément de données stocké dans la liste. Ce qui est spécial avec les types génériques, c’est que vous les codez une seule fois, mais vous pouvez les utiliser avec différents paramètres. Cela présente des avantages significatifs : vous réutilisez vos efforts de développement et de test, sans compromettre la sécurité des types et les performances, et sans surcharger votre 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<K,T>, défini comme suit :

[C#]

public class LinkedList<K,T>
{...}

[Visual Basic]

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

[C++]

generic <typename 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 l’élément de données stocké dans la liste. L’utilisation de paramètres de type génériques permet à la liste liée de différer la décision sur les types réels à utiliser. En fait, il appartient 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 que le client spécifie à utiliser à la place du paramètre de type. Par exemple, compte tenu de cette définition de type générique et de cette déclaration :

[C#]

public class MyClass<T>
{...}
MyClass<int> obj = new  MyClass<int>();

[Visual Basic]

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

[C++]

generic <typename T>
public ref class MyClass
{...};
MyClass<int> ^obj = gcnew  MyClass<int>;

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 tout type générique qui a au moins un argument de type.

Par exemple, étant donné cette définition de liste liée générique :

[C#]

public class LinkedList<T>
{...}

[Visual Basic]

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

[C++]

generic <typename T>
public ref class LinkedList
{...};

Voici un type générique construit :

[C#]

LinkedList<string>

[Visual Basic]

LinkedList(Of String)

[C++]

LinkedList<String ^>

Pour être considéré comme un type construit, vous pouvez également spécifier des paramètres de type pour le type générique :

[C#]

public class MyClass<T>
{
   LinkedList<T> m_List; //Constructed type 
}

[Visual Basic]

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

[C++]

generic <typename T>
public ref class MyClass
{
   LinkedList<T> ^m_List; //Constructed type 
};

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

Un type construit ouvert est tout type générique qui contient au moins un paramètre de type utilisé comme argument de type. Par exemple, en fonction de cette définition :

[C#]

public class LinkedList<K,T>
{...}

[Visual Basic]

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

[C++]

generic <typename K, typename T>
public ref class LinkedList
{...};

Ensuite, les déclarations suivantes des variables membres LinkedList<K,T> sont toutes des types construits ouverts :

[C#]

public class MyClass<K,T>
{
   LinkedList<K,T>      m_List1; //Open constructed type 
   LinkedList<K,string> m_List2; //Open constructed type 
   LinkedList<int,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 <typename K, typename T>
public ref class MyClass
{
   LinkedList<K, T>      ^m_List1; //Open constructed type 
   LinkedList<K, String ^> ^m_List2; //Open constructed type 
   LinkedList<int, 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 aucun paramètre de type comme arguments de type. Par exemple, en fonction de cette définition :

[C#]

public class LinkedList<K,T>
{...}

[Visual Basic]

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

[C++]

generic <typename K, typename T>
public ref class LinkedList
{...};

Ensuite, les déclarations suivantes des variables membres LinkedList<K,T> sont toutes des types construits fermés :

[C#]

LinkedList<int,string> list1; //Closed constructed type 
LinkedList<int,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<int, String ^> ^list1; //Closed constructed type 
LinkedList<int, 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. Ces arguments sont appelés 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<K,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 <typename 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 éléments de données stockés 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<int,string> list = new LinkedList<int,string>();
list.AddHead(123,"ABC");

[Visual Basic]

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

[C++]

LinkedList<int, String ^> ^list = gcnew LinkedList<int, 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 a déjà des arguments de type, comme LinkedList<int,string> , est appelé type construit.

Lorsque vous spécifiez des arguments de type pour des types génériques, vous pouvez en fait fournir des paramètres de type. Par exemple, considérez cette définition de la classe Node<K,T> , qui est utilisée comme nœud dans une liste liée :

[C#]

class Node<K,T>
{
   public K Key;
   public T Item;
   public Node<K,T> NextNode;

   public Node(K key,T item,Node<K,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 <typename K, typename T>
ref class Node
{
public: 
   K Key;
   T Item;
   Node<K,T> ^NextNode;

   Node(K key,T item,Node<K,T> ^nextNode)
   {
      Key      = key;
      Item     = item;
      NextNode = nextNode;
   }
};

La classe Node<K,T> contient en tant que variable membre une référence au nœud suivant. Ce membre doit être fourni avec le type à utiliser à la place de ses paramètres de type générique. Le nœud spécifie ses propres paramètres de type dans ce cas.

Un autre exemple de spécification de paramètres de type générique pour un type générique est la façon dont la liste liée elle-même peut déclarer et utiliser le nœud :

[C#]

public class LinkedList<K,T>
{
   Node<K,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 <typename K, typename T>
public ref class LinkedList
{
   Node<K,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 purement à des fins de lisibilité, afin de rendre l’utilisation du nœud plus cohérente. Vous avez peut-être défini la liste liée avec n’importe quel autre nom de paramètre de type générique. Dans ce cas, vous devez également les transmettre au nœud :

[C#]

public class LinkedList<Key,Item>
{
   Node<Key,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 <typename Key, typename Item>
public ref class LinkedList
{
   Node<Key,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 à usage général, des collections ou des classes utilitaires, vous devez baser tous ces éléments sur un objet. Par exemple, voici l’interface IList basée sur l’objet, qui se trouve 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 les types valeur tels que les entiers ou les types référence tels que les 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

Toutefois, étant donné que IList est basé sur l’objet, chaque utilisation d’un type valeur force la boxing dans un objet et le déballe lors de l’utilisation de l’indexeur. L’utilisation de types référence force l’utilisation d’un cast qui complique le code et a un impact sur les performances.

Considérez maintenant l’interface d’équivalent générique, IList<T>, qui se trouve dans l’espace de noms System.Collections.Generic :
[C#]

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

[Visual Basic]

<DefaultMember("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 <typename T>
public interface class IList : ICollection<T>
{
   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 cet IList<T> peuvent également l’utiliser pour manipuler des listes liées de n’importe quel type, mais sans aucune pénalité de performances. Lors de l’utilisation d’un type valeur au lieu des paramètres de type, aucun boxing ou unboxing n’est effectué, et lors de l’utilisation d’un type référence, aucun cast n’est nécessaire :

[C#]

IList<int> numbers = new List<int>();
numbers.Add(1);
int number = numbers[0];

IList<string> names = new List<string>();
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<int> ^numbers = gcnew List<int>;
numbers->Add(1);
int number = numbers[0];

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

Divers benchmarks ont montré que dans les modèles d’appel intense, les génériques génèrent en moyenne une amélioration des performances de 200 % lors de l’utilisation de types valeur, et d’environ 100 % d’amélioration des performances lors de l’utilisation de types référence.

Toutefois, les performances ne sont pas l’avantage main des génériques. Dans la plupart des applications réelles, les goulots de bouteille tels que les E/S masquent les avantages des génériques en matière de performances. L’avantage le plus important des génériques est la sécurité de type. Avec les solutions basées sur des objets, l’incompatibilité de type est toujours respectée, mais génère une erreur au moment 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 notoirement difficiles à rechercher et à résoudre. Avec les génériques, ce code n’est jamais compilé :

[C#]

IList<int> numbers = new List<int>();
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<int> ^numbers = gcnew List<int>;
numbers->Add(1);
String ^name = numbers[0]; //Compile-time error

Pourquoi ne puis-je pas utiliser Type-Specific structures de données au lieu de génériques ?

Pour éviter le problème de sécurité de type sans génériques, vous pouvez être tenté d’utiliser des interfaces et une structure de données spécifiques 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 avec cette approche est que vous aurez besoin d’une interface et d’une implémentation spécifiques au type par type de données avec lequel vous devez interagir, comme une chaîne ou un client. Si vous avez un défaut dans votre gestion des éléments de données, vous devrez le corriger dans autant d’endroits que de types, ce qui est simplement sujet aux erreurs et peu pratique. Avec les génériques, vous pouvez définir et implémenter votre logique une seule fois, tout en l’utilisant avec le type de votre choix.

Quand dois-je utiliser des génériques ?

Vous devez utiliser des génériques chaque fois que vous avez la possibilité de le faire. Autrement dit, si une structure de données ou une classe utilitaire propose une version générique, vous devez utiliser la version générique, et non les méthodes basées sur les objets. La raison en est que les génériques offrent des avantages significatifs, notamment la productivité, la sécurité de type et les performances, sans aucun coût pour vous. En règle générale, les collections et les structures de données telles que les listes liées, les files d’attente, les arborescences binaires, etc. offrent une 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 utilitaires telles que les fabriques de classes ou les formateurs tirent également parti des génériques. Le seul cas où vous ne devez pas tirer parti des génériques est le ciblage croisé. Si vous développez votre code pour cibler .NET 1.1 ou une version antérieure, vous ne devez utiliser aucune des nouvelles fonctionnalités de .NET 2.0, y compris les génériques. Dans C# 2.0, vous pouvez même indiquer au compilateur les paramètres du projet (sous Générer | Avancé) pour utiliser uniquement la syntaxe C# 1.0 (ISO-1).

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

Les types génériques ne sont pas covariants. Cela signifie que vous ne pouvez pas remplacer un type générique par un argument de type spécifique, par un autre type générique qui utilise un argument de type qui est le type de base pour le premier argument de type. Par exemple, l’instruction suivante ne se compile pas :

[C#]

class MyBaseClass
{}
class MySubClass : MyBaseClass
{}
class MyClass<T>
{}
//Will not compile 
MyClass<MyBaseClass> obj = new MyClass<MySubClass>();

[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 <typename T> where T : MyBaseClass
ref class MyClass
{};
//Will not compile 
MyClass<MySubClass ^> ^obj = gcnew MyClass<MyBaseClass ^>;

[C#]

En utilisant la même définition que dans l’exemple ci-dessus, il est également vrai que MyClass<MyBaseClass> n’est pas le type de base de MyClass<MySubClass> :

Debug.Assert(typeof(MyClass<MyBaseClass>) != typeof(MyClass<MySubClass>).BaseType);

[Visual Basic]

En utilisant 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++]

En utilisant la même définition que dans l’exemple ci-dessus, il est également vrai que MyClass<MyBaseClass> n’est pas le type de base de MyClass<MySubClass> :

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

Ce ne serait pas le cas si les types génériques étaient des contre-variantes.

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

Par exemple, l’instruction suivante ne se compile pas :

[C#]

class MyBaseClass<T>
{
   public virtual T MyMethod()
   {...}
}
class MySubClass<T,U> : MyBaseClass<T> 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 comme contrainte.

Cela dit, les contraintes sont covariantes. Par exemple, vous pouvez satisfaire une contrainte à l’aide d’un sous-type du type de la contrainte :

[C#]

class MyBaseClass
{}
class MySubClass : MyBaseClass
{}
class MyClass<T> where T : MyBaseClass
{}

MyClass<MySubClass> obj = new MyClass<MySubClass>();

[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 <typename T> where T : MyBaseClass
ref class MyClass 
{};

MyClass<MySubClass ^> ^obj = gcnew MyClass<MySubClass ^>;

Vous pouvez encore restreindre davantage les contraintes de cette façon :

[C#]

class BaseClass<T>  where T : IMyInterface
{}
interface IMyOtherInterface : IMyInterface
{}
 
class SubClass<T> : BaseClass<T> 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 <typename T> where T : IMyInterface
ref class BaseClass  
{};
interface class IMyOtherInterface : IMyInterface
{};
generic <typename T> where T : IMyOtherInterface
ref class SubClass : BaseClass<T>
{};

Enfin, les génériques sont invariants, car il n’existe aucune relation entre deux types génériques avec des arguments de type différents, même si ces arguments de type ont une relation is-as, par exemple, List<int> n’a rien à voir avec l’objet> List<, même si un 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<T> : IEnumerator,IDisposable
{
   T Current{get;}
}

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

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

public delegate void EventHandler<E>(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 <typename T>
public interface class IEnumerator : IEnumerator,IDisposable
{
   property T Current { T get(); }
};

generic <typename T>
public ref class List : IList<T> //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 <typename K, typename V>
public ref struct KeyValuePair
{
public: 
   KeyValuePair(K key, V value);   
   K Key;
   V Value;
};

generic <typename T> where T: EventArgs
public delegate void EventHandler(Object ^sender, T e);

En outre, les méthodes statiques et instance peuvent s’appuyer sur des paramètres de type génériques, indépendamment des types qui les contiennent :

[C#]

public sealed class Activator : _Activator
{
   public static T CreateInstance<T>();
   //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 <typename T>
           static T CreateInstance();
   //Additional members 
};

D’autre part, les énumérations ne peuvent pas définir les paramètres de type, et il en va de même pour les attributs.

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

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

[C#]

public class MyClass
{
   public void MyInstanceMethod<T>(T t)
   {...}
   public static void MyStaticMethod<T>(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 <typename T>
   void MyInstanceMethod (T t)
   {...}
   generic <typename 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 en passant chaque fois différents types de paramètres, sans jamais surcharger la méthode. Lorsque vous appelez une méthode qui définit des paramètres de type génériques, vous devez fournir les arguments de type sur le site d’appel :

[C#]

MyClass obj = new MyClass();
obj.MyInstanceMethod<int>(3);
obj.MyInstanceMethod<string>("Hello");

MyClass.MyStaticMethod<int>(3);
MyClass.MyStaticMethod<string>("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<int>(3);
obj->MyInstanceMethod<String ^>("Hello");

MyClass::MyStaticMethod<int>(3);
MyClass::MyStaticMethod<String ^>("Hello");

Si l’inférence de type est disponible, vous pouvez omettre de spécifier 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"));

Puis-je dériver d’un paramètre de type générique ?

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

[C#]

public class MyClass<T> : T //Does not compile 
{...}

[Visual Basic]

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

[C++]

generic <typename T>
public ref class MyClass : T //Does not compile 
{...};

Qu’est-ce qu’une inférence de type générique ?

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

[C#]

public class MyClass
{
   public void MyInstanceMethod<T>(T t)
   {...}
   public static void MyStaticMethod<T>(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 <typename T>      
   void MyInstanceMethod(T t) {...}
   generic <typename T>   
   static void MyStaticMethod (T t) {...}
};

Lorsque vous appelez ces méthodes, vous pouvez omettre de spécifier les arguments de type pour les méthodes instance et 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 n’est possible que lorsque la méthode prend un argument des arguments de type déduits. Par exemple, dans la méthode CreateInstance<T>() de la classe Activateor, définie comme suit :

[C#]

public sealed class Activator : _Activator
{
   public static T CreateInstance<T>();
   //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 <typename 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<MyClass>(); 

[Visual Basic]

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

[C++]

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

Notez également que vous ne pouvez pas compter sur l’inférence de type au niveau du type, uniquement au niveau de la méthode. Dans l’exemple suivant, vous devez toujours fournir l’argument de type T même si la méthode prend un paramètre T :

[C#]

public class MyClass<T>
{
   public static void MyStaticMethod<U>(T t,U u)
   {...}
}
MyClass<int>.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 <typename T>
public ref class MyClass
{
   public: generic <typename U> static void MyStaticMethod (T t,U u)
   {...}
};
MyClass<int>::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 de types autorisés à être utilisés comme arguments de type, mais en même temps, elles ajoutent 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 lui-même. En d’autres termes, les contraintes empêchent le client de spécifier des types en tant qu’arguments de type qui n’offrent pas les méthodes, les propriétés ou les membres des paramètres de type générique sur lesquels le type générique s’appuie.

Après avoir appliqué une contrainte, intelliSense reflète les contraintes lors de l’utilisation du paramètre de type générique, par exemple, en suggérant des méthodes ou des 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 dérivent 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<T> sur son paramètre de type générique. Cela est nécessaire pour que vous puissiez implémenter une recherche. Fonctionnalité de tri ou d’indexation dans la liste :

[C#]

class Node<K,T>
{
   public K Key;
   public T Item;
   public Node<K,T> NextNode;
} 

public class LinkedList<K,T> where K : IComparable<K>
{
   Node<K,T> m_Head;

   public T this[K key]
   {
      get
      {
         Node<K,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 <typename K, typename T>
ref class Node
{
public: K Key;
        T Item;
        Node<K,T> ^NextNode;
};
generic <typename K, typename T> where K : IComparable<K>
public ref class LinkedList
{
public:
   Node<K,T> ^m_Head;
public: 
   property T default[]
   {
      T get(K key)
      {
         Node<K,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<K,T> where K : IComparable<K>
                             where T : ICloneable 

[Visual Basic]

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

[C++]

generic <typename K, typename T> where K : IComparable<K>
                      where T : ICloneable
public ref class LinkedList  
{ ... };                         

Vous pouvez avoir une contrainte de classe de base, c’est-à-dire stipulant que le paramètre de type générique dérive d’une classe de base particulière :

[C#]

public class MyBaseClass
{...}
public class MyClass<T> 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 <typename T> where T : MyBaseClass
public ref class MyClass
{...};

Toutefois, vous ne pouvez utiliser qu’une seule classe de base au maximum dans une contrainte, car ni C#, Visual Basic ni C++ managé ne prennent en charge l’héritage multiple de l’implémentation. Évidemment, la classe de base à laquelle vous êtes contraint ne peut pas être une classe scellée, et le compilateur l’applique. En outre, vous ne pouvez pas limiter System.Delegate ou System.Array en tant que classe de base.

Vous pouvez limiter à 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<K,T> where K : MyBaseKey,IComparable<K>
{...}

[Visual Basic]

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

[C++]

generic <typename K, typename T> where K : MyBaseKey, IComparable<K>
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 (constructeur public sans paramètre). Par exemple :

[C#]

class Node<K,T> where K : new()
               where T : new()
{
   public K Key;
   public T Item;
   public Node<K,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 des contraintes de dérivation, à condition que la contrainte de constructeur par défaut apparaisse en dernier dans la liste des contraintes :

[C#]

public class LinkedList<K,T> where K : IComparable<K>,new()
                             where T : new() 
{...}

[Visual Basic]

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

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

[C#]

public class MyClass<T> where T : struct 

{...}

[Visual Basic]

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

De même, vous pouvez limiter un paramètre de type générique à un type référence (classe) :

[C#]

public class MyClass<T> where T : class 

{...}

[Visual Basic]

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

La contrainte de type référence et valeur cLa contrainte de type valeur/référence ne peut pas être utilisée avec une contrainte de classe de base, mais elle peut être combinée avec 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 bien que les contraintes soient 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 de type sécurisé et autorise uniquement l’accès aux fonctionnalités au niveau de l’objet dans vos paramètres de type générique. Les contraintes font partie des métadonnées de type générique afin que le compilateur côté client puisse également en tirer parti. Le compilateur côté client permet uniquement au développeur client d’utiliser des types conformes aux contraintes, appliquant ainsi la sécurité de type.

Avec quoi ne puis-je pas utiliser les contraintes ?

Vous pouvez uniquement placer une contrainte de dérivation sur un paramètre de type (qu’il s’agit 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. Bien que tout le reste ne soit implicitement pas autorisé, il convient de mentionner les cas spécifiques qui ne sont pas possibles :

  • Vous ne pouvez pas contraindre un type générique à avoir une construction paramétrable spécifique.
  • Vous ne pouvez pas contraindre un type générique à dériver d’une classe scellée.
  • Vous ne pouvez pas contraindre un type générique à dériver d’une classe statique.
  • Vous ne pouvez pas contraindre un type générique public à dériver d’un autre type interne.
  • Vous ne pouvez pas contraindre un type générique à avoir une méthode spécifique, qu’il s’agit d’une méthode statique ou d’une méthode instance.
  • Vous ne pouvez pas limiter un type générique à un événement public spécifique.
  • Vous ne pouvez pas contraindre un paramètre de type générique à dériver de System.Delegate ou System.Array.
  • Vous ne pouvez pas contraindre un type générique parameterfundamentals_topic2 à sérialisable.
  • Vous ne pouvez pas limiter la visibilité COM d’un type générique parameterfundamentals_topic2.
  • Vous ne pouvez pas contraindre un paramètre de type générique à avoir un attribut particulier.
  • Vous ne pouvez pas limiter un paramètre de type générique pour prendre en charge un opérateur spécifique. Il n’existe donc aucun moyen de compiler le code suivant :

[C#]

public class Calculator<T>
{
   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 <typename 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 des énumérations, des structs ou des classes sealed comme contraintes génériques ?

Vous ne pouvez pas contrainter un paramètre de type générique à dériver d’un type non dérivable. Par exemple, les éléments suivants ne sont pas compilés :

[C#]

public sealed class MySealedClass
{...}
public class MyClass<T> 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 <typename T> where T : MySealedClass
public ref class MyClass //Does not compile 
{...};

La raison est simple : les seuls arguments de type qui pourraient éventuellement satisfaire la contrainte ci-dessus sont le type MySealedClass lui-même, 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 enums ne sont pas autorisés dans les contraintes.

Le code qui utilise des génériques est-il plus rapide que le code qui ne le fait 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 comme conteneurs amorphes pour stocker des éléments, différents benchmarks ont montré que dans les modèles d’appel intense, les génériques génèrent en moyenne une amélioration des performances de 100 % (c’est-à-dire trois fois plus rapide) lors de l’utilisation de types valeur, et d’environ 50 % d’amélioration des performances lors de l’utilisation de types référence.

Si le code non générique utilise des structures de données spécifiques au type, les génériques n’ont aucun avantage en matière de performances. Toutefois, ce code est intrinsèquement très fragile. L’écriture d’une structure de données spécifique au type est une tâche fastidieuse, répétitive et sujette aux erreurs. Lorsque vous corrigez un défaut dans la structure de données, vous devez le corriger non seulement à un emplacement, mais à autant d’endroits qu’il existe des doublons spécifiques au type de ce qui est essentiellement la même structure de données.

Une application qui utilise des génériques est-elle plus rapide qu’une application qui ne le fait pas ?

En fonction de l’application bien sûr, mais d’une manière générale, dans la plupart des applications réelles, les goulots de bouteille tels que les E/S masquent les avantages en matière de performances des génériques. Le véritable avantage des génériques n’est pas les performances, mais plutôt la sécurité de type et la productivité.

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

Les génériques sont similaires au concept des modèles C++ classiques : les deux permettent aux structures de données ou aux classes utilitaires de différer au client les types réels à utiliser, et offrent tous deux des avantages en matière de productivité et de sécurité de type.

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

Il existe deux différences main : dans le modèle de programmation et dans l’implémentation sous-jacente. Dans le modèle de programmation, les génériques .NET peuvent offrir une sécurité renforcée par rapport aux modèles Visual C++ classiques. Les génériques .NET ont la notion de contraintes, ce qui vous donne une sécurité de type supplémentaire. D’autre part, les génériques .NET offrent un modèle de programmation plus restrictif: il existe plusieurs choses que les génériques ne peuvent pas faire, telles que l’utilisation d’opérateurs, car il n’existe aucun moyen de contrainte d’un paramètre de type pour prendre en charge un opérateur. Ce n’est pas le cas dans les modèles Visual C++ classiques où vous pouvez appliquer n’importe quel opérateur de votre choix sur les paramètres de type. Au moment de la compilation, le compilateur Visual C++ classique remplace tous les paramètres de type dans le modèle par le type spécifié, et toute incompatibilité est généralement découverte à ce moment-là.

Les modèles et les génériques peuvent entraîner une certaine surcharge du code, et les deux ont des mécanismes pour limiter ce ballonnement. L’instanciation d’un modèle avec un ensemble spécifique de types instancie uniquement les méthodes réellement utilisées ; puis toutes les méthodes qui aboutissent à du code identique sont automatiquement fusionnées par le compilateur, ce qui empêche 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 référence ; Le ballonnement provient uniquement des types valeur, car le CLR instancie un générique séparément une fois pour chaque argument de type valeur. Enfin, les génériques .NET vous permettent d’envoyer des fichiers binaires, tandis que les modèles C++ vous obligent à partager du 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 services et un fournisseur de services. Tant que le consommateur programme strictement contre l’interface (et non une implémentation particulière de celle-ci), il peut utiliser n’importe quel autre fournisseur de services qui prend en charge la même interface. Cela permet de changer de fournisseur de services sans affecter (ou avec un effet minimal sur) le code du client. L’interface permet également au même fournisseur de services de fournir des services à différents clients. Les interfaces sont la pierre angulaire de l’ingénierie logicielle moderne, et sont largement utilisées 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 valider les types réels utilisés. Par conséquent, les interfaces et les génériques ne s’excluent pas mutuellement. Loin de là, ils se complimentent mutuellement. Vous pouvez et vous devez combiner des interfaces et des génériques.

Par exemple, l’interface ILinkedList<T> définie comme suit :

[C#]

public interface ILinkedList<T>
{
   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 <typename T>
public interface class ILinkedList
{
   void AddHead(T item);
   void RemoveHead(T item);
   void RemoveAll();
};

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

[C#]

public class LinkedList<T> : ILinkedList<T>
{...}

public class MyOtherLinkedList<T> : ILinkedList<T>
{...}

[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 <typename T>
public ref class LinkedList : ILinkedList<T>
{...};

generic <typename T>
public ref class MyOtherLinkedList : ILinkedList<T>
{...};

Vous pouvez maintenant programmer sur ILinkedList<T>, à l’aide d’implémentations différentes et d’arguments de type différents :

[C#]

ILinkedList<int> numbers  = new LinkedList<int>();
ILinkedList<string> names = new LinkedList<string>();

ILinkedList<int> moreNumbers = new MyOtherLinkedList<int>();

[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<int> ^numbers    = gcnew LinkedList<int>;
ILinkedList<String ^> ^names = gcnew LinkedList<String ^>;

ILinkedList<int> ^moreNumbers = gcnew MyOtherLinkedList<int>();

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

Les génériques ont une prise en charge native dans IL et le CLR lui-même. Lorsque vous compilez du code générique côté serveur, le compilateur le compile en IL, comme n’importe quel autre type. Toutefois, l’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é de type. Lorsque le client fournit des arguments de type, le compilateur du client remplace le paramètre de type générique dans les métadonnées du serveur par le type spécifié. Cela fournit au compilateur du client une définition spécifique au type du serveur, comme si les génériques n’ont jamais été impliqués. Au moment de l’exécution, le code d’ordinateur réel produit dépend si les types spécifiés sont de type valeur ou de référence. Si le client spécifie un type de valeur, le compilateur JIT remplace les paramètres de type générique dans l’il par le type de valeur spécifique et le compile en code natif. Toutefois, le compilateur JIT effectue le suivi du code de serveur spécifique au type qu’il a déjà généré. Si le compilateur JIT est invité à compiler le serveur générique avec un type de valeur qu’il a déjà compilé sur le code de l’ordinateur, il retourne simplement une référence à ce code de serveur. Étant donné que le compilateur JIT utilise le même code de serveur spécifique au type de valeur dans toutes les autres rencontres, il n’y a pas d’augmentation du code.

Si le client spécifie un type de référence, le compilateur JIT remplace les paramètres génériques du serveur IL par un objet et le compile dans du code natif. Ce code sera utilisé dans toutes les demandes supplémentaires pour un type de référence au lieu d’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 hors du tas managé, et il n’y a pas de cast.

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

La raison est simple : étant donné qu’il n’existe aucun moyen de contraindre un paramètre de type générique à prendre en charge un opérateur, il n’existe aucun moyen pour le compilateur de déterminer si le type spécifié par le client du type générique prend en charge l’opérateur.

Considérez par exemple le code suivant :

[C#]

class Node<K,T>
{
   public K Key;
   public T Item;
   public Node<K,T> NextNode;
} 

public class LinkedList<K,T> 
{
   Node<K,T> m_Head;

   public T this[K key]
   {
      get
      {
         Node<K,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

<DefaultMember("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 <typename K, typename T>
ref class Node
{
public: K Key;
        T Item;
        Node<K,T> ^NextNode;
};

generic <typename K, typename T>
public ref class LinkedList 
{
public:
   Node<K,T> ^m_Head;
public:
   property T default[]
   {
      T get(K key)
      {
         Node<K,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))

Parce qu’il n’a aucun moyen de savoir si le type que le consommateur spécifiera prendra en charge l’opérateur ==.

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

Vous pouvez utiliser un opérateur (ou, d’ailleurs, n’importe quelle méthode spécifique au type) sur des paramètres de type générique si le paramètre de type générique est contraint d’être un type qui prend 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<T> 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<T>: Attribute
{...}

[Visual Basic]

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

[C++]

//This is not possible:
generic <typename T>
ref class MyAttribute: Attribute
{...};

Toutefois, 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 publication de .NET 2.0, les génériques feront partie du CLS.

 

À propos de l’auteur

Juval Lowy est architecte logiciel et principal d’IDesign, spécialisé dans le conseil en architecture .NET et la formation avancée .NET. Juval est le directeur régional de Microsoft pour la Silicon Valley, travaillant avec Microsoft pour aider le secteur à adopter .NET. Son dernier livre est Programming .NET Components 2nd Edition (O’Reilly, 2005). Juval participe aux révisions de conception interne de Microsoft pour les futures versions de .NET. Juval a publié de nombreux articles, concernant presque tous les aspects du développement .NET, et est un présentateur fréquent lors de conférences de développement. Microsoft a reconnu Juval comme une légende logicielle comme l’un des meilleurs experts .NET et leaders du secteur au monde.