Generics en C# : pourquoi sont-ils nécessaires et comment les utiliser en pratique
Les Generics (génériques) sont l'une des fonctionnalités clés du langage C#, qui permet d'écrire un code flexible, réutilisable et type-safe. Dans cet article, nous allons voir pourquoi les génériques sont nécessaires, comment ils fonctionnent sous le capot, et montrer des exemples pratiques de leur utilisation dans des projets réels.
Que sont les Generics et pourquoi sont-ils nécessaires ?
Imaginez que vous devez implémenter une méthode qui échange deux valeurs. Sans génériques, vous devriez écrire des versions séparées pour chaque type de données :
// Sans Generics — duplication de codevoid SwapInt(ref int a, ref int b) { int temp = a; a = b; b = temp; }void SwapString(ref string a, ref string b) { string temp = a; a = b; b = temp; }// ... et ainsi de suite pour chaque typeAvec les Generics, ce problème est résolu élégamment :
// Avec Generics — une seule méthode pour tous les typesvoid Swap<T>(ref T a, ref T b){ T temp = a; a = b; b = temp;}
// Utilisationint x = 1, y = 2;Swap(ref x, ref y); // T = int
string s1 = "Bonjour", s2 = "Monde";Swap(ref s1, ref s2); // T = stringPrincipaux avantages des Generics :
- Sécurité des types — les erreurs de type sont détectées à la compilation
- Performances — absence de boxing/unboxing pour les types valeur
- Réutilisation du code — une classe/méthode fonctionne avec n'importe quel type
- Lisibilité — le code devient plus clair et concis
Classes et structures génériques
L'exemple le plus courant d'utilisation des Generics est celui des collections. Au lieu d'écrire une classe pour chaque type, nous créons une classe générique :
public class MyList<T>{ private T[] _items; private int _count;
public MyList(int capacity = 4) { _items = new T[capacity]; _count = 0; }
public void Add(T item) { if (_count == _items.Length) { Array.Resize(ref _items, _items.Length * 2); } _items[_count++] = item; }
public T Get(int index) { if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); return _items[index]; }
public int Count => _count;}
// Utilisationvar numbers = new MyList<int>();numbers.Add(10);numbers.Add(20);
var names = new MyList<string>();names.Add("Alice");Remarque : lors de la création d'une instance, nous spécifions le type concret entre chevrons <T>. Le compilateur substitue ce type partout où T est utilisé.
Méthodes génériques et contraintes (constraints)
Il est parfois nécessaire d'imposer des contraintes sur le type T. Par exemple, pour garantir que le type implémente une certaine interface ou possède un constructeur sans paramètres :
// Contrainte : T doit implémenter IComparable<T>public T GetMax<T>(T a, T b) where T : IComparable<T>{ return a.CompareTo(b) > 0 ? a : b;}
// Contrainte : T doit avoir un constructeur sans paramètrespublic class Factory<T> where T : new(){ public T Create() { return new T(); }}
// Contrainte : T doit être un type valeur (struct)public struct Nullable<T> where T : struct{ private T _value; private bool _hasValue;
public Nullable(T value) { _value = value; _hasValue = true; }
public T Value => _hasValue ? _value : throw new InvalidOperationException(); public bool HasValue => _hasValue;}Principaux types de contraintes :
where T : class— type référencewhere T : struct— type valeurBlogs
Book Recommendations
Lire aussi
Sécurité des applications TypeScript : top 10 des vulnérabilités en pratique
Injection de dépendances (Dependency Injection) en TypeScript : Guide complet
Concurrence et multithreading en PHP : niveau avancé
Meilleurs IDE et outils pour le développement Kotlin en 2025
Optionnels implicitement déballés : pourquoi en avons-nous besoin si nous connaissons la valeur ?