Les génériques permettent d'écrire une fois un type ou une méthode et de l'utiliser avec de nombreux types, sans perdre la sûreté de typage. Au lieu d'une List d'object qui demande des casts et accepte n'importe quoi, vous obtenez List<int> et List<Order>, chacune vérifiée par le compilateur. L'emplacement entre chevrons, par convention T, est un paramètre de type ; le type que fournit un appelant est l'argument de type.
Pourquoi les génériques : le problème qu'ils résolvent
Avant les génériques (C# 1), les collections stockaient des object. Cela compilait, mais les erreurs n'apparaissaient qu'à l'exécution, et chaque int était boxé à l'entrée :
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
Avec List<decimal>, le second Add est une erreur de compilation, la boucle n'a besoin d'aucun cast, et les décimaux sont stockés comme des décimaux.
Classes génériques
Déclarez les paramètres de type après le nom de la classe, puis utilisez-les comme n'importe quel autre type dans la classe :
Sortie :
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> et Page<int> sont deux types différents produits à partir d'une seule définition. Une classe peut prendre plusieurs paramètres de type (Dictionary<TKey, TValue>) ; nommez-les T quand il n'y en a qu'un et TSomething quand il y en a plusieurs.
Méthodes génériques et inférence de type
Une méthode peut avoir ses propres paramètres de type, que sa classe soit générique ou non. Les appelants omettent en général l'argument de type, car le compilateur le déduit des arguments :
Sortie :
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
L'inférence ne fonctionne qu'à partir des arguments, jamais du type de retour. Repeat(0, 3) n'a besoin d'aucune annotation, mais une méthode comme T Create<T>() n'a rien à partir de quoi déduire, donc les appelants doivent écrire Create<Customer>(). La plus grande partie de LINQ est faite de méthodes génériques qui reposent sur cette inférence : prices.Where(p => p > 1) est Where<decimal> sans que vous ayez à l'écrire.
Contraintes : ce que T a le droit d'être
Dans une méthode générique, le compilateur ne vous laisse faire que ce qui fonctionne pour tout T possible. Sans contrainte, c'est à peine plus que ce que permet object. Pour appeler CompareTo, créer une instance ou lire une propriété, indiquez au compilateur ce que doit être T, avec where :
Sortie :
17
plum
19.99
42 Zoe
Les contraintes que vous utiliserez :
| Contrainte | Signification | Vous permet de |
|---|---|---|
where T : class | type référence | affecter null, utiliser as |
where T : struct | type valeur non nullable | utiliser T? comme Nullable<T> |
where T : new() | constructeur public sans paramètre | appeler new T() (doit être listée en dernier) |
where T : Entity | dérive de Entity | utiliser les membres d'Entity |
where T : IComparable<T> | implémente l'interface | appeler CompareTo |
where T : U | dérive d'un autre paramètre de type | relier deux paramètres |
where T : unmanaged (C# 7.3) | struct sans référence à l'intérieur | pointeurs, stackalloc |
where T : Enum (C# 7.3) | un type enum | passer aux méthodes d'Enum |
where T : notnull (C# 8) | type non nullable | annotations de référence nullable |
Plusieurs contraintes sur un même paramètre se séparent par des virgules ; plusieurs paramètres ont chacun leur propre clause where : class Cache<TKey, TValue> where TKey : notnull where TValue : class.
Une contrainte manquante se manifeste par une erreur de compilation à l'endroit où vous utilisez le membre, par exemple CS1061 ('T' does not contain a definition for 'CompareTo') ou CS0304 en appelant new T() sans new().
default(T)
Le code générique a parfois besoin de « la valeur vide » d'un type qu'il ne connaît pas. default(T) donne 0 pour les nombres, false pour bool, null pour les types référence et nullables, et une struct à zéro sinon :
Sortie :
31
0
Porto
True
0001-01-01
La deuxième ligne montre la faiblesse de ce schéma : pour les types valeur, « introuvable » et « trouvé un 0 » se ressemblent. C'est pourquoi les méthodes du framework utilisent plutôt la forme TryGet (bool TryGetValue(TKey key, out TValue value)). Depuis C# 7.1, vous pouvez écrire le plus court return default; quand le type est clair d'après le contexte.
Les membres statiques existent par type construit
Chaque type construit (Counter<int>, Counter<string>) reçoit sa propre copie des champs statiques de la classe :
Sortie :
2
1
False
C'est parfois utile (un cache par type comme TypeInfo<T>.Name, calculé une fois par T) et parfois une surprise quand vous attendiez un compteur partagé. Placez l'état partagé dans une classe non générique.
Collections génériques
Les collections génériques couvrent la plupart des besoins : List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, et les interfaces qu'elles partagent, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. Acceptez l'interface la plus étroite dont une méthode a besoin (IEnumerable<T> si elle ne fait que boucler), et renvoyez un type concret ou en lecture seule.
Covariance et contravariance
Un Dog est un Animal, mais une List<Dog> n'est pas une List<Animal> :
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
Si c'était permis, animals.Add(new Cat()) mettrait un chat dans la liste de chiens. Les interfaces qui ne font que produire des valeurs sont sûres, et on les déclare covariantes avec out :
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
L'image inverse, la contravariance, marquée in, s'applique aux interfaces et délégués qui ne font que consommer des valeurs : une Action<Animal> peut être utilisée là où une Action<Dog> est attendue, car ce qui sait traiter n'importe quel animal sait traiter un chien. IComparer<in T> est contravariante, et Func<in T, out TResult> utilise les deux marqueurs. La variance ne s'applique qu'aux interfaces et aux délégués, et seulement pour des arguments de type référence : IEnumerable<int> ne se convertit pas en IEnumerable<object>.
Erreurs courantes
- Utiliser
objectlà où un paramètre de type s'impose. On renonce aux vérifications de compilation et on boxe les types valeur. - Comparer deux valeurs
Tavec==. Sans contrainteclass, cela ne compile pas (CS0019), et avec elle cela compare des références. UtilisezEqualityComparer<T>.Default.Equals(a, b), qui fonctionne pour toutTet respecte le propreEqualsdu type. - Renvoyer
default(T)pour « introuvable ». C'est ambigu pour les types valeur. Utilisezbool TryX(..., out T value). - Trop de contraintes. Chacune restreint qui peut utiliser la méthode. Ne contraignez que ce dont le corps a réellement besoin.
- S'attendre à ce que
List<Derived>se convertisse enList<Base>. Acceptez plutôtIEnumerable<Base>ouIReadOnlyList<Base>.
Questions fréquentes
Que sont les génériques en C# ?
Les génériques permettent d'écrire une seule fois une classe, une interface ou une méthode avec un type substituable, T, que les appelants renseignent : List<int>, List<Customer>. Le compilateur vérifie chaque usage avec le type réel, vous obtenez donc la sûreté de typage sans casts, et les types valeur sont stockés sans boxing. Les collections de System.Collections.Generic en sont l'exemple le plus familier.
Comment écrire une méthode générique en C# ?
Placez les paramètres de type après le nom de la méthode : static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. Les appelants omettent en général l'argument de type, car le compilateur le déduit des arguments : Largest(prices) fonctionne quand prices est une List<decimal>. Écrivez-le explicitement, Largest<decimal>(...), quand il n'y a rien à partir de quoi le déduire.
Que signifie where T : class en C# ?
C'est une contrainte : T doit être un type référence, donc le code générique peut affecter null à un T et utiliser as T. Les autres contraintes sont where T : struct (un type valeur non nullable), where T : new() (a un constructeur public sans paramètre, donc le code peut appeler new T()), where T : SomeBaseClass et where T : ISomeInterface, qui permet au code d'appeler les membres de cette interface sur T.
Qu'est-ce que default(T) en C# ?
La valeur par défaut du type que se révèle être T : 0 pour les nombres, false pour bool, null pour les types référence et nullables, et une struct à zéro pour les autres types valeur. Le code générique l'utilise quand il a besoin de « pas de valeur » sans connaître le type, par exemple comme valeur de retour d'une recherche infructueuse. C# 7.1 a ajouté le littéral default, plus court.
Pourquoi ne peut-on pas affecter une List<Dog> à une List<Animal> en C# ?
Parce qu'une List<Animal> accepte n'importe quel animal : si l'affectation était permise, du code pourrait ajouter un Cat à ce qui est en réalité une List<Dog>. Le compilateur la refuse (CS0029). Les interfaces en lecture seule sont covariantes, donc IEnumerable<Animal> animals = dogs; et IReadOnlyList<Animal> animals = dogs; compilent tous deux, puisque rien ne peut être ajouté à travers elles.