Types génériques
Introduction
Les types génériques permettent de combiner la réutilisabilité du code et la sécurité du type. Les types génériques sont le plus souvent employés dans le cadre de collections. Le Framework .NET expose ces collections dans l’espace de noms System.Collections.Generic comme le type List<T> ou Dictionary<TKey, TValue>. Bien entendu, il est possible de créer ses propres types génériques afin de fournir une solution adaptée.
L’avantage des types génériques par rapport aux collections classiques comme le type ArrayList est que le type des objets est conservé alors qu’une collection non générique stocke les données en faisant une conversion en type Object. La récupération d’un élément d’une collection classique oblige à faire la conversion inverse pour correspondre avec le type attendu tandis que les types génériques retournent un objet déjà typé. Malgré la complexité de codage légèrement supérieure, les types génériques apportent, en plus de la sûreté, beaucoup plus de rapidité, surtout lorsque les éléments de la liste sont des types valeur.
La création de types génériques
Un type générique est défini dans la déclaration de la classe en plaçant le paramètre T. Ce paramètre spécifie que le type sera choisi par le consommateur de la classe. Cela peut être un type valeur ou un type référence.
Créez une nouvelle classe ReportChangeList<T> dans le dossier Library du projet :
public class ReportChangeList<T>
{
}
Le paramètre T accepte un type qui sera spécifié au moment de l’instanciation :
ReportChangeList<int> Ex1 = new ReportChangeList<int>();
ReportChangeList<Object> Ex2 = new ReportChangeList<Object>();
Un type générique peut également faire référence à plusieurs classes :
public class ClasseGenerique<T, U>
Les types génériques peuvent être surchargés tant que le nombre de leurs paramètres de type n’est pas identique :
public class ClasseGenerique<T>
public class ClasseGenerique<T, U>
Si deux types génériques ont le même nom et le même nombre de paramètres de type, le compilateur lèvera une erreur :
public class ClasseGenerique<T>
public class ClasseGenerique<U> // Non autorisé...
Les contraintes de type
Comme vu dans les accesseurs, l’interface IReportChange est utilisée pour accéder à la propriété HasChanged des éléments de la liste. Cela signifie que les éléments ajoutés doivent obligatoirement implémenter l’interface. Pour limiter les types qui pourront se substituer au paramètre générique T, des contraintes peuvent être appliquées.
Voici les contraintes existantes :
Contrainte |
Description |
where T : classe |
Contrainte sur la classe de base du type. |
where T : interface |
Contrainte sur l’interface implémentée par le type. |
where T : class |
Le type doit être un type référence. |
where T : struct |
Le type doit être une structure. |
where T : new() |
Le type doit avoir un constructeur sans paramètre. |
where U : T |
Le type représenté par U doit être identique au type T. |
Ajoutez une contrainte sur l’interface IReportChange sur la classe générique ReportChangeList :
public class ReportChangeList<T> : IReportChildrenChange where T :
IReportChange
Les contraintes sont appliquées sur tous les paramètres de type définis, que ce soit dans les méthodes ou dans la définition de type.
Les contraintes de type peuvent également être définies au niveau des méthodes :
public void MaMethode<T>()...
Les interfaces génériques
Comme pour les listes, il serait intéressant de pouvoir effectuer une boucle foreach sur les éléments de notre type générique. Pour cela, il suffit d’implémenter l’interface générique IEnumerable<T> :
public class ReportChangeList<T> : IReportChildrenChange,
IEnumerable<T> where T : IReportChange
Cette interface générique se comporte comme une interface classique, elle contient les signatures des membres requis pour son implémentation et ceux-ci peuvent utiliser la classe générique comme référence.
Ajoutez les membres GetEnumerator() et IEnumerable.GetEnumerator() suivants pour implémenter l’interface dans la classe générique ReportChangeList :
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < this.children.Count; i++)
{
yield return this.children[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
L’implémentation d’un énumérateur consiste à effectuer une boucle sur les éléments de la liste et à retourner chacun d’eux à l’appelant. Le mot-clé yield return permet de faire un retour à l’appelant avec la valeur à retourner en fonction de la précédente valeur retournée. L’état de la méthode est maintenu de telle manière qu’elle peut continuer son exécution au prochain appel. La durée de vie de cet état est liée à l’énumérateur, l’état de la méthode est supprimé lorsque l’énumération est terminée.
1. La variance dans les interfaces génériques
La covariance et la contravariance sont des concepts déjà utilisés depuis la version 2.0 du Framework .NET....
Les attributs génériques
Un attribut générique est une manière de déclarer une classe générique dont la classe de base est System.Attribute. Cette syntaxe est pratique pour les attributs qui implémentent un paramètre de type System.Type. La déclaration de la classe se fait avec la syntaxe suivante :
public class AttributGenerique<T> : Attribute { }
Utiliser un attribut générique se fait de la même manière que pour un attribut classique en spécifiant le paramètre de type :
[AttributGenerique<int>]
public int MaMethode() { }
Un attribut générique peut spécifier des paramètres dans son constructeur. Il devient obligatoire, dans ce cas, de les spécifier lors de l’utilisation de l’attribut générique :
public class AttributGenerique<T> : Attribute
{
public AttributGenerique(int i) { }}
L’utilisation de l’attribut avec paramètres se fera de la manière suivante :
[AttributGenerique<int>(10)]
public int MaMethode() { }
De la même manière que l’opérateur typeof, l’attribut générique ne peut concerner ni un type dynamic, ni un type référence nullable (exemple :...
La création de méthodes génériques
La déclaration des membres d’une classe générique se fait de la même manière que les membres de classes classiques avec en plus l’utilisation du paramètre T pour spécifier le type d’un paramètre qui est autorisé.
Ajoutez la méthode Add à la classe ReportChangeList :
public void Add(T Child)
{
IReportChange child = (IReportChange)Child;
child.Changed += new EventHandler(ChildChanged);
this.children.Add(Child);
}
La méthode Add prend en paramètre un type répondant aux contraintes de la classe. L’objet passé en paramètre à la méthode implémente donc l’interface IReportChange et il peut être converti dans le type de l’interface, ce qui permet à l’objet de type ReportChangeList de s’abonner à l’événement Changed de l’objet passé en paramètre avant de l’ajouter à la liste children.
La liste va contenir de nombreux éléments et il faut donc implémenter une technique plus élaborée pour les différencier et assurer l’unicité de ceux-ci. Nous allons créer un indexeur basé sur une clé...
Valeur par défaut générique
En étudiant de près l’accesseur get de l’indexeur de la classe ReportChangeList, vous pouvez remarquer que si aucun élément de la liste ne correspond à la clé passée en paramètre, la valeur de retour est :
return default(T);
Un type générique peut concerner un type valeur ou un type référence. Les types valeur n’étant pas nullable (ne pouvant pas avoir de valeur null), il est impossible de retourner null pour l’accesseur get. Le mot-clé default est utilisé pour obtenir la valeur par défaut du paramètre type. Ainsi, pour un paramètre de type référence, la valeur null sera retournée et pour un type valeur, il s’agira de sa valeur par défaut. Si T représente le type int, la valeur par défaut retournée sera zéro.
L’héritage de classe générique
Une classe générique peut hériter d’une autre classe. Les déclarations des classes enfants peuvent jouer avec les paramètres de type de la classe parente :
-
La classe héritée peut garder le même paramètre de type :
class Parent<T>
{ }
class Enfant<T> : Parent<T>
{ }
-
La classe héritée peut également spécifier le paramètre de type :
class Enfant : Parent<int>
{ }
-
La classe héritée peut introduire de nouveaux paramètres de type :
class Enfant<T, T2> : Parent<T>
{ }
-
Le nom du paramètre de type du parent peut être renommé et utilisé dans la déclaration du type enfant :
class Enfant<Tx, T2> : Parent<Tx>
{ }