Aller au contenu principal

Génériques avancés, délégués et événements

Notions théoriques

Génériques avancés

Vous avez déjà vu List<T> et Dictionary<TKey, TValue>. Vous pouvez créer vos propres classes et méthodes génériques avec des contraintes sur le type T.

// Interface marqueur pour les entités avec identifiant
public interface IIdentifiable
{
int Id { get; }
}

// Dépôt générique contraint : T doit implémenter IIdentifiable
public class Depot<T> where T : IIdentifiable
{
private readonly List<T> _elements = new();

public void Ajouter(T element) => _elements.Add(element);
public T? Trouver(int id) => _elements.FirstOrDefault(e => e.Id == id);
public List<T> TousLesElements() => _elements.ToList();
}

Méthode générique

public static T? TrouverPremier<T>(List<T> liste, Func<T, bool> predicat)
{
return liste.FirstOrDefault(predicat);
}

// Utilisation
var guerrier = TrouverPremier(personnages, p => p.Type == "Guerrier");

Contraintes disponibles

ContrainteSignification
where T : classT doit être un type référence
where T : structT doit être un type valeur
where T : new()T doit avoir un constructeur sans paramètre
where T : IIdentifiableT doit implémenter cette interface
where T : PersonnageT doit hériter de cette classe

Délégués

Un délégué est un type qui représente une référence vers une méthode. Il définit la signature attendue (paramètres et type de retour).

// Déclaration d'un type délégué
delegate int CalculOperation(int a, int b);

// Méthodes correspondant à la signature
int Additionner(int a, int b) => a + b;
int Multiplier(int a, int b) => a * b;

// Utilisation
CalculOperation op = Additionner;
Console.WriteLine(op(3, 4)); // 7

op = Multiplier;
Console.WriteLine(op(3, 4)); // 12

Délégués génériques intégrés

En pratique, vous n'aurez presque jamais besoin de déclarer vos propres délégués. .NET fournit trois types génériques qui couvrent tous les cas :

DéléguéSignatureExemple d'utilisation
Func<T, TResult>Prend T, retourne TResultFunc<Personnage, int> getForce = p => p.Force;
Func<T1, T2, TResult>Prend T1 et T2, retourne TResultFunc<int, int, int> add = (a, b) => a + b;
Action<T>Prend T, retourne voidAction<string> afficher = s => Console.WriteLine(s);
Predicate<T>Prend T, retourne boolPredicate<Personnage> estVivant = p => p.PointsDeVie > 0;

Événements

Les événements sont construits sur les délégués et implémentent le pattern Observer : un objet notifie d'autres objets quand quelque chose se produit.

public class Personnage
{
public string Nom { get; set; } = "";
public int PointsDeVie { get; private set; } = 100;

// Déclaration de l'événement (Action<string, int> = nom du personnage + dégâts reçus)
public event Action<string, int>? PersonnageBlessé;

public void RecevoirDegats(int degats)
{
PointsDeVie -= degats;
// Déclencher (invoke) l'événement si des abonnés existent
PersonnageBlessé?.Invoke(Nom, degats);
}
}

S'abonner et se désabonner

var guerrier = new Personnage { Nom = "Aragorn" };

// Abonnement avec +=
guerrier.PersonnageBlessé += (nom, degats) =>
Console.WriteLine($"{nom} a reçu {degats} points de dégâts !");

guerrier.RecevoirDegats(20);
// Affiche : Aragorn a reçu 20 points de dégâts !

// Désabonnement avec -= (nécessite une référence à la méthode)
void Journaliser(string nom, int degats) => Console.WriteLine($"[LOG] {nom} : -{degats} PDV");
guerrier.PersonnageBlessé += Journaliser;
guerrier.PersonnageBlessé -= Journaliser; // se désabonne
info

Le mot-clé event protège le délégué : les abonnés extérieurs ne peuvent que s'abonner (+=) ou se désabonner (-=). Seule la classe propriétaire peut déclencher l'événement (Invoke).

Exemple pratique

using System;
using System.Collections.Generic;
using System.Linq;

// Interface marqueur
public interface IIdentifiable { int Id { get; } }

// Entité
public class Personnage : IIdentifiable
{
public int Id { get; set; }
public string Nom { get; set; } = "";
public int PointsDeVie { get; private set; } = 100;

// Événement : déclenché à chaque fois que le personnage reçoit des dégâts
public event Action<Personnage, int>? PersonnageBlessé;

public void RecevoirDegats(int degats)
{
PointsDeVie = Math.Max(0, PointsDeVie - degats);
PersonnageBlessé?.Invoke(this, degats);
}
}

// Dépôt générique
public class Depot<T> where T : IIdentifiable
{
private readonly List<T> _elements = new();

public void Ajouter(T element) => _elements.Add(element);
public T? Trouver(int id) => _elements.FirstOrDefault(e => e.Id == id);
public List<T> Tous() => _elements.ToList();
}

class Program
{
static void Main()
{
var depot = new Depot<Personnage>();

var aragorn = new Personnage { Id = 1, Nom = "Aragorn" };
var legolas = new Personnage { Id = 2, Nom = "Legolas" };

depot.Ajouter(aragorn);
depot.Ajouter(legolas);

// Abonnement à l'événement
aragorn.PersonnageBlessé += (p, d) =>
Console.WriteLine($"[EVENT] {p.Nom} a reçu {d} dégâts ! PDV restants : {p.PointsDeVie}");

aragorn.RecevoirDegats(30);
aragorn.RecevoirDegats(50);

// Utilisation du dépôt générique
var trouve = depot.Trouver(2);
Console.WriteLine($"Trouvé : {trouve?.Nom ?? "introuvable"}");

// Func et Action
Func<Personnage, bool> estVivant = p => p.PointsDeVie > 0;
Action<Personnage> afficher = p => Console.WriteLine($" {p.Nom} : {p.PointsDeVie} PDV");

Console.WriteLine("=== Personnages vivants ===");
depot.Tous().Where(estVivant).ToList().ForEach(afficher);
}
}

Test de mémorisation/compréhension


Que signifie la contrainte 'where T : IIdentifiable' dans une classe générique ?


Quel type de délégué générique utilise-t-on pour une méthode qui prend un Personnage et retourne un bool ?


Avec quel opérateur s'abonne-t-on à un événement en C# ?


Quelle est la différence entre un délégué et un événement (event) en C# ?


Quel type de délégué prend un paramètre et ne retourne rien ?


TP pour réfléchir et résoudre des problèmes

Dans ce TP, vous allez implémenter un événement PersonnageBlessé déclenché à chaque attaque dans le projet RPG.

Étape 1 — Ajouter l'événement à la classe Personnage

Ajoutez un événement PersonnageBlessé de type Action<Personnage, int> à la classe Personnage.


Bonne pratique - Déclarer les événements avec le type nullable

Le ? après le type du délégué (Action<Personnage, int>?) indique que l'événement peut être null (aucun abonné). Cela est requis avec les Nullable Reference Types activés (C# 8+) et force à utiliser l'opérateur ?. lors du déclenchement.

Étape 2 — Déclencher l'événement lors d'une attaque

Implémentez la méthode RecevoirDegats qui réduit les points de vie et déclenche l'événement.


Bonne pratique - Math.Max pour éviter les points de vie négatifs

Math.Max(0, PointsDeVie - degats) garantit que les points de vie ne passent jamais en dessous de 0, ce qui évite des comportements inattendus (affichage de valeurs négatives, logique de "revie" involontaire).

Étape 3 — S'abonner à l'événement et journaliser les combats

Dans Main, créez un personnage, abonnez-vous à son événement et simulez plusieurs attaques.


Bonne pratique - Séparer les préoccupations avec les événements

Grâce à l'événement, la classe Personnage ne connaît pas le système de journalisation, l'interface utilisateur, ni le système de score. Elle se contente de signaler ce qui se passe. Chaque partie du code qui s'intéresse à cette information s'abonne de son côté. C'est le principe du pattern Observer.

📌 Une solution