Que sont les Generics en C# et pourquoi sont-ils nécessaires
Imaginez que vous écrivez une méthode qui doit fonctionner avec différents types de données : des nombres entiers, des chaînes de caractères, des objets. Sans Generics (génériques), vous devriez soit dupliquer le code pour chaque type, soit utiliser object avec un cast de type ultérieur. Les deux approches entraînent des problèmes : la première conduit à la redondance, la seconde à des erreurs d'exécution.
Les Generics (types génériques) sont un mécanisme en C# qui permet de créer des classes, des méthodes, des interfaces et des délégués fonctionnant avec n'importe quel type de données, tout en conservant un typage fort. Au lieu d'écrire du code pour un type spécifique, vous utilisez un paramètre de type (généralement désigné par la lettre T), qui est remplacé par le type réel lors de l'utilisation.
Les principaux avantages des Generics :
- Sécurité des types — le compilateur vérifie la validité de l'utilisation des types à la compilation
- Performance — absence d'encapsulation/désencapsulation (boxing/unboxing) pour les types valeur
- Réutilisation du code — une seule classe/méthode fonctionne avec plusieurs types
- Lisibilité — le code devient plus propre et plus compréhensible
Problème sans Generics : exemple avec object
Avant l'apparition des Generics, les développeurs utilisaient souvent le type object pour créer des méthodes universelles. Regardons un exemple :
public class Stack{ private object[] _items = new object[10]; private int _count = 0; public void Push(object item) { _items[_count++] = item; } public object Pop() { return _items[--_count]; } // Utilisation Stack stack = new Stack(); stack.Push(123); // Encapsulation de int en object stack.Push("hello"); // N'importe quel type peut être ajouté int number = (int)stack.Pop(); // Désencapsulation et cast}Les problèmes sont évidents :
- Lors de l'extraction, un cast explicite est nécessaire
- On peut accidentellement ajouter une valeur d'un autre type
- Pour les types valeur (int, double), il y a encapsulation/désencapsulation, ce qui réduit les performances
Fondamentaux des Generics en C#
Création d'une classe générique
Reprenons le même exemple de pile, mais avec les Generics :
public class Stack<T>{ private T[] _items = new T[10]; private int _count = 0; public void Push(T item) { _items[_count++] = item; } public T Pop() { return _items[--_count]; }}
// UtilisationStack<int> intStack = new Stack<int>();intStack.Push(123);intStack.Push(456);int number = intStack.Pop(); // Pas besoin de désencapsulation, le type est connu
Stack<string> stringStack = new Stack<string>();stringStack.Push("Hello");// stringStack.Push(123); // Erreur de compilation ! Sécurité des typesLe paramètre <T> est spécifié entre chevrons. Lors de la création d'une instance, vous spécifiez le type concret, et le compilateur génère une version spécialisée de la classe pour ce type.
Méthodes génériques
Les Generics fonctionnent non seulement avec les classes, mais aussi avec des méthodes individuelles :
public class Utils{ public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; }}
// Utilisationint maxInt = Utils.Max(5, 10); // 10string maxStr = Utils.Max("apple", "banana"); // "banana"Contraintes dans les Generics
Parfois, il est nécessaire de limiter les types pouvant être utilisés avec un générique. Pour cela, on utilise des contraintes (constraints) avec le mot-clé where :
// Contrainte : T doit être un type référencepublic class Repository<T> where T : class{