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
| Contrainte | Signification |
|---|---|
where T : class | T doit être un type référence |
where T : struct | T doit être un type valeur |
where T : new() | T doit avoir un constructeur sans paramètre |
where T : IIdentifiable | T doit implémenter cette interface |
where T : Personnage | T 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é | Signature | Exemple d'utilisation |
|---|---|---|
Func<T, TResult> | Prend T, retourne TResult | Func<Personnage, int> getForce = p => p.Force; |
Func<T1, T2, TResult> | Prend T1 et T2, retourne TResult | Func<int, int, int> add = (a, b) => a + b; |
Action<T> | Prend T, retourne void | Action<string> afficher = s => Console.WriteLine(s); |
Predicate<T> | Prend T, retourne bool | Predicate<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
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
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.
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.
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.
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.