Generics en C# : pourquoi sont-ils nécessaires et comment les utiliser en pratique

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

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 type


Avec 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 = string


Principaux 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 :