1. Introduction
Imagine que t'as une liste d'étudiants, et tu dois les trier une fois par âge, puis par nom de famille, puis par moyenne, mais seulement pour ceux qui ont réussi tous les exams. Si tu dois écrire une nouvelle classe qui implémente IComparer<T> pour chaque comparaison "one-shot", ton projet va vite devenir une décharge de petites classes comparateurs. Pas cool : le code devient lourd et illisible.
Pour ce genre de cas, C# propose une solution plus élégante : tu peux passer la logique de comparaison direct dans la méthode Sort sans créer de classe à part.
Pour ça, on va avoir besoin des delegates et de leurs cousins compacts – les expressions lambda.
Les delegates – nos assistants flexibles
Avant de passer aux lambdas, voyons ce que c'est un delegate. En gros, un delegate, c'est un type qui représente une référence vers une méthode. Ça sonne un peu mystique, non ? Imagine-le comme ça :
T'as une to-do list, et certains trucs dessus sont des "instructions" ou "recettes". Un delegate, c'est comme une variable spéciale qui peut stocker la référence à une de ces "recettes" (méthodes). Et quand tu veux exécuter ce truc, tu utilises la variable delegate, et elle "appelle" la méthode à laquelle elle fait référence.
En C#, les delegates servent à créer des callbacks (appeler une méthode plus tard, souvent en réaction à un événement), gérer des événements (genre cliquer sur un bouton), et bien sûr, passer des méthodes comme arguments à d'autres méthodes (pour qu'une méthode puisse en appeler une autre), ce qui est exactement ce qu'on veut pour le tri.
La méthode List<T>.Sort() a plusieurs surcharges (versions), et l'une d'elles prend un delegate spécial appelé Comparison<T>.
2. Delegate Comparison<T>
C'est quoi Comparison<T> ?
Comparison<T> — c'est un delegate intégré dans .NET, fait exprès pour comparer deux objets du même type T. Sa "recette" : il prend deux objets de type T (appelons-les x et y) et retourne un entier (int) :
- Un nombre négatif (genre -1), si x est "plus petit" que y.
- Zéro (0), si x est "égal" à y.
- Un nombre positif (genre 1), si x est "plus grand" que y.
C'est exactement comme ça que bossent IComparable.CompareTo et IComparer.Compare. Donc, même logique, sauf que maintenant tu peux la passer comme "variable-méthode", pas besoin de classe à part.
Regardons un exemple. Revenons à nos étudiants. Disons qu'on a une classe Student :
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public double AverageGrade { get; set; }
public Student(string firstName, string lastName, int age, double averageGrade)
{
FirstName = firstName;
LastName = lastName;
Age = age;
AverageGrade = averageGrade;
}
public void PrintInfo()
{
Console.WriteLine($"Étudiant : {FirstName} {LastName}, Âge : {Age}, Note : {AverageGrade:F2}");
}
}
Maintenant, pour trier la liste des étudiants par âge en utilisant un delegate, on peut écrire une méthode statique séparée qui correspond à la signature de Comparison<Student> :
public class Program
{
// Méthode qui correspond à la signature du delegate Comparison<Student>
// Elle va comparer deux étudiants par leur âge
public static int CompareStudentsByAge(Student student1, Student student2)
{
// On utilise la méthode intégrée CompareTo pour les nombres,
// qui retourne -1, 0 ou 1 selon la comparaison.
return student1.Age.CompareTo(student2.Age);
}
public static void Main(string[] args)
{
List<Student> students = new List<Student>
{
new Student("Ivan", "Petrov", 20, 4.5),
new Student("Maria", "Sidorova", 22, 4.8),
new Student("Alexey", "Ivanov", 19, 3.9),
new Student("Elena", "Kozlova", 20, 4.2) // Deux étudiants du même âge
};
Console.WriteLine("--- Liste des étudiants avant le tri ---");
foreach (var s in students)
s.PrintInfo();
Console.WriteLine("--- On trie les étudiants par âge (avec un delegate) ---");
students.Sort(CompareStudentsByAge); //on passe la méthode CompareStudentsByAge comme paramètre
foreach (var s in students)
s.PrintInfo();
}
}
Décryptage du code :
- On a créé une méthode statique CompareStudentsByAge qui prend deux étudiants et retourne un int, selon le contrat Comparison<Student>.
- Dans Main on a créé une liste d'étudiants.
- Quand on appelle students.Sort(CompareStudentsByAge);, on n'appelle pas la méthode CompareStudentsByAge() tout de suite ! On passe juste une référence à cette méthode. List<T>.Sort() va ensuite appeler notre méthode CompareStudentsByAge autant de fois qu'il faut pour trier, en passant différentes paires d'étudiants. C'est comme filer une adresse de livraison, pas envoyer tout le camion d'un coup.
Cette approche est bien plus pratique que de créer une classe comparateur pour chaque petit tri. Mais on peut faire encore plus court !
3. Place aux Expressions Lambda
Même devoir écrire une méthode séparée comme CompareStudentsByAge peut sembler de trop si la logique de comparaison est simple et ne sert qu'une ou deux fois. Pour ça, C# a introduit les expressions lambda (lambda expressions).
C'est quoi une expression lambda ? En gros, c'est une méthode anonyme ou, comme j'aime dire, une "méthode sans domicile". C'est une façon d'écrire un petit bout de code (méthode) direct là où t'en as besoin, sans la déclarer à part. C'est comme écrire vite fait une note sur un post-it et la coller sur la tâche, au lieu de rédiger tout un manuel.
L'opérateur principal des lambdas, c'est => (ça se lit "flèche" ou "va vers"). Il sépare les paramètres de l'action de la méthode.
Syntaxe de base d'une expression lambda
Disons que t'as un delegate (référence vers une méthode) et tu le passes à la fonction Sort() :
public static int CompareStudentsByAge(Student student1, Student student2)
{
return student1.Age.CompareTo(student2.Age);
}
students.Sort(CompareStudentsByAge); //on passe la méthode CompareStudentsByAge comme paramètre
On peut écrire ça plus court :
//on passe une méthode anonyme comme paramètre
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );
Ici, au lieu du nom de la méthode, on met juste ses deux trucs importants :
- les paramètres : (Student student1, Student student2)
- le contenu de la méthode : student1.Age.CompareTo(student2.Age)
Cette écriture compacte, c'est une expression lambda : (paramètres) => expression
Comment ça marche
Le compilateur C#, quand il voit une expression lambda, va générer une vraie méthode à partir de ça.
Par exemple, si t'as ce code :
students.Sort( (s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade) );
Le résultat à la compilation sera à peu près :
public static int CompareStudents_Lambda123(Student s1, Student s2)
{
return s2.AverageGrade.CompareTo(s1.AverageGrade);
}
students.Sort( CompareStudents_Lambda123 );
4. Exemple de tri et expressions lambda
Réécrivons notre exemple avec les étudiants, mais avec une expression lambda :
public class Program
{
public static void Main(string[] args)
{
List<Student> students = new List<Student>
{
new Student("Ivan", "Petrov", 20, 4.5),
new Student("Maria", "Sidorova", 22, 4.8),
new Student("Alexey", "Ivanov", 19, 3.9),
new Student("Elena", "Kozlova", 20, 4.2)
};
Console.WriteLine("--- Liste des étudiants avant le tri ---");
foreach (var s in students)
s.PrintInfo();
// Maintenant la logique de comparaison est écrite direct ici, "sur place"
Console.WriteLine("--- On trie les étudiants par âge (avec une lambda) ---");
students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
foreach (var s in students)
s.PrintInfo();
// Pour trier par ordre décroissant, on inverse juste le résultat
Console.WriteLine("\n--- On trie les étudiants par moyenne (décroissant) ---");
// s2.CompareTo(s1) au lieu de s1.CompareTo(s2)
students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));
foreach (var s in students)
s.PrintInfo();
}
}
Qu'est-ce qui se passe ici ?
- students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
- student1 et student2 — ce sont les paramètres que Sort va passer à notre méthode anonyme (pareil que x et y dans Comparison<T>).
- => — c'est l'opérateur lambda.
- student1.Age.CompareTo(student2.Age) — c'est le corps de la lambda. Ici, c'est juste une expression, et c'est ça qui est retourné.
- Pour trier par moyenne en décroissant, on inverse juste s1 et s2 dans CompareTo. C'est le classique pour inverser l'ordre du tri.
Pourquoi c'est pratique ?
- Compact : Pas besoin de créer des méthodes ou classes séparées pour chaque petite logique de tri.
- Lisible : La logique de tri est juste à côté de l'appel à Sort(), donc c'est plus facile à comprendre, surtout pour les cas simples.
- Flexible : Tu peux changer les conditions de tri à la volée.
5. Delegates et Lambdas – le duo parfait
Tu te demandes peut-être : alors une expression lambda, c'est pareil qu'un delegate, ou pas ?
En vrai, une expression lambda, c'est juste du syntactic sugar pour créer une instance de delegate ou un arbre d'expression (Expression Tree, on verra ça plus tard). Quand le compilateur voit une lambda, il la transforme "sous le capot" en une instance du delegate qui va bien. Dans notre cas, comme List<T>.Sort() attend un delegate Comparison<T>, le compilateur pige que (student1, student2) => student1.Age.CompareTo(student2.Age) doit devenir un Comparison<Student>.
Du coup, les lambdas te permettent d'écrire du code super concis, et les delegates sont les "conteneurs" qui transportent ce code et le font tourner. Ils bossent main dans la main !
Quand utiliser quoi ?
- IComparable<T> : Utilise-le quand ton type a un ordre naturel, évident. Par exemple, si tu tries des produits, et que l'ordre principal c'est par leur référence. Cet interface définit l'ordre "par défaut".
- IComparer<T> : Utilise-le quand t'as besoin d'une logique de tri réutilisable, plusieurs fois, mais tu veux pas "polluer" la classe principale ou quand t'as plusieurs façons de trier. Par exemple, un IComparer pour trier par prix, un autre par nom, et tu les utilises à différents endroits du programme.
- Delegates (Comparison<T>) et Lambdas : Parfaits pour les tris one-shot, ad-hoc, quand la logique est simple et ne mérite pas une classe à part. C'est la façon la plus courante et propre pour la plupart des tris en C#. C'est aussi top pour passer de la logique à d'autres méthodes, genre les méthodes de filtrage (Find, FindAll) ou de recherche (FindIndex), qu'on a vues avant.
| Caractéristique | IComparable<T> | IComparer<T> | Comparison<T> / Lambda |
|---|---|---|---|
| Où c'est défini ? | Dans la classe T elle-même | Dans une classe comparateur séparée | Peut être une méthode ou une expression anonyme |
| Flexibilité | Ordre "naturel" fixé | Plusieurs ordres réutilisables | Ad-hoc (à la volée), pour un appel précis |
| Boilerplate | Petit, dans la classe | Moyen (classe séparée) | Minimal (surtout pour les lambdas) |
| Exemple d'utilisation | |
|
|
| Lisibilité | Top pour l'ordre naturel | Dépend du nom du comparateur | Nickel pour les comparaisons simples et spécifiques |
6. Application pratique et regard vers l'avenir
Les expressions lambda, c'est pas juste du "syntactic sugar" pour le tri. C'est un outil puissant, utilisé partout dans le code C# moderne. Tu vas les croiser tout le temps :
- Dans LINQ (Language Integrated Query) : C'est sûrement l'usage le plus massif des lambdas. LINQ te permet d'écrire des requêtes façon SQL sur des collections, et les lambdas servent à définir les conditions de filtrage, de tri, de projection des données. On va bientôt voir LINQ, et tu verras comment les lambdas le rendent super puissant et pratique.
- Dans la gestion d'événements : Les lambdas permettent de décrire en une ligne ce qui doit se passer lors d'un événement (genre un clic sur un bouton dans une UI).
- En asynchrone : Pour définir des tâches à exécuter en parallèle.
- Dans plein d'API .NET : Beaucoup de méthodes de la bibliothèque standard .NET prennent des delegates (et donc des lambdas) comme paramètres pour ajouter de la logique flexible.
Bref, maîtriser les lambdas, c'est pas juste mieux trier : c'est un gros pas pour comprendre le code C# moderne et ses libs. C'est une compétence qui sera appréciée en entretien et utile dans tous tes projets !
7. Erreurs classiques et subtilités
Quand tu bosses avec des delegates et des lambdas pour comparer, y'a quelques trucs à surveiller :
Mauvais résultat de comparaison : Souviens-toi que CompareTo ou ta logique de comparaison doit retourner un nombre négatif, zéro ou positif. Si tu renvoies autre chose, le tri peut déconner ou même planter. L'erreur la plus fréquente : les débutants renvoient true ou false au lieu d'un int. La méthode Sort attend un nombre, parce qu'elle a besoin de savoir non seulement si les éléments sont égaux, mais aussi lequel est "plus grand".
Gestion des valeurs null : Si ta collection peut contenir des null, essayer d'appeler une méthode sur un objet null (genre student1.Age.CompareTo(...) si student1 est null) va lancer une NullReferenceException. Dans ce cas, ta logique de comparaison doit gérer explicitement les null. En général, null est considéré comme "plus petit" que tout ce qui n'est pas null. Si les deux sont null, ils sont égaux. Si un seul est null, il est "plus petit".
// Exemple de gestion des null dans une lambda de comparaison
students.Sort((s1, s2) => {
if (s1 == null && s2 == null) return 0;
if (s1 == null) return -1; // null est plus petit que tout
if (s2 == null) return 1; // non-null est plus grand que null
return s1.Age.CompareTo(s2.Age); // On compare si les deux sont non-null
});
Heureusement, dans la vraie vie, les collections n'ont pas souvent de null, mais c'est important d'y penser !
Performance : Même si les lambdas sont super pratiques, les utiliser à outrance dans des boucles très "chaudes" ou sur d'énormes collections peut avoir un petit impact sur la perf comparé à des classes IComparer ultra-optimisées et testées. Mais pour la plupart des cas, la différence est négligeable, et le gain en lisibilité et simplicité du code l'emporte largement.
Chaînes de comparaisons complexes : Comme on l'a vu avec le tri par nom puis prénom, les lambdas permettent d'imbriquer plusieurs conditions. C'est bien plus pratique que d'écrire dix if à la suite ! Le principal : toujours vérifier le résultat de la première comparaison (lastNameComparison != 0) avant de passer au niveau suivant.
Les expressions lambda et les delegates, c'est des concepts de base en C# qui ouvrent la porte à un style de prog plus flexible et fonctionnel. Les comprendre et savoir les utiliser rendra ton code bien plus propre, efficace et moderne. Continue à expérimenter, et bientôt tu les utiliseras sans même y penser !
GO TO FULL VERSION