1. Intro
Imagine que t'es le doyen d'une fac où y'a une tonne d'étudiants. T'as souvent besoin de listes triées selon différents critères :
- Par nom (pour retrouver vite un étudiant dans l'ordre alphabétique).
- Par moyenne (pour filer des bourses aux meilleurs).
- Par âge (pour les stats, concours, etc).
- Par année, et à l'intérieur de l'année — par nom de famille.
Si on se contentait de IComparable<T>, notre classe Étudiant devrait n'avoir qu'une seule façon de se comparer. Genre, on décide que l'ordre "naturel" d'un étudiant, c'est par moyenne. Nickel, List.Sort() marche ! Mais si on veut une liste triée par nom ? La classe Étudiant est déjà "occupée" à comparer par moyenne. Elle peut pas avoir deux ordres "naturels" en même temps. C'est comme si t'avais qu'une seule notice "comment être le meilleur en boxe", et d'un coup tu dois devenir champion d'échecs, et tu tentes d'utiliser la notice de boxe. Pas sûr que ça marche, hein ?
C'est justement pour ces cas-là, quand tu veux trier le même type d'objets de plusieurs façons et que t'as pas envie de foutre toute la logique de comparaison dans la classe, qu'il existe l'interface IComparer<T>. Elle est super utile si t'as pas accès au code source de la classe ou si la classe doit pas connaître tous les moyens de tri.
2. Interface IComparer<T>
Si IComparable<T> — c'est comme l'instinct interne de l'objet, la façon dont il sait se comparer lui-même à un autre, alors IComparer<T> — c'est carrément un juge externe ou un arbitre indépendant, qui prend deux joueurs (objets) au hasard et les compare selon ses propres règles.
Imagine que t'es coach d'une équipe de foot. Tu dois choisir qui sera capitaine.
- IComparable<T> : Chaque joueur dit lui-même : "Je suis meilleur que ce gars parce que je cours plus vite !" (Sa propre règle interne).
- IComparer<T> : Toi, le coach, tu dis : "Ok les gars, aujourd'hui on choisit le capitaine selon la précision des passes. Pierre, fais une passe ! Victor, fais une passe ! Super, Pierre est plus précis ! Il est capitaine aujourd'hui." (C'est ta règle externe que tu appliques à n'importe quels deux joueurs).
Définition : IComparer<T> — c'est une interface dans .NET qui te permet de définir une logique de comparaison externe pour deux objets du type T. Ça veut dire que la classe qui implémente IComparer<T> n'est pas un des objets comparés ; elle fournit juste la méthode Compare qui prend deux objets et décide de leur ordre relatif.
Syntaxe
La syntaxe de l'interface IComparer<T> est super simple :
public interface IComparer<T>
{
// Méthode qui compare deux objets de type T
// x - premier objet à comparer
// y - deuxième objet à comparer
int Compare(T x, T y);
}
La méthode Compare(T x, T y) marche exactement comme la méthode CompareTo(T other) dans IComparable<T> :
- Retourne un nombre négatif si x est "plus petit" que y.
- Retourne zéro (0) si x est "égal" à y.
- Retourne un nombre positif si x est "plus grand" que y.
Dans notre cas, "plus petit", "égal", "plus grand" dépendent uniquement de la logique de comparaison qu'on va définir dans l'implémentation de Compare.
Quelle différence entre IComparer<T> et IComparable<T> ?
| Classe / Interface | Où c'est implémenté | À quoi ça sert | Exemple d'utilisation |
|---|---|---|---|
|
Directement dans le type (classe/struct) | Une seule façon standard de comparer | Tri par ordre croissant d'ID |
|
Dans une classe séparée | Autant de façons de comparer que tu veux | Tri par nom, date, etc. |
3. Implémenter IComparer<T> en pratique
On continue à faire évoluer notre "grosse appli" — un modèle simple d'utilisateurs. Disons qu'on a cette classe :
// Notre classe utilisateur
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
Tri par nom : on code un comparateur à part
On va écrire une classe spéciale qui implémente IComparer<User> et compare les users par nom :
// Classe comparateur pour trier par nom
public class UserNameComparer : IComparer<User>
{
public int Compare(User x, User y)
{
// On check le null (pour éviter les surprises !)
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1; // null est "plus petit" que n'importe quel objet
if (y is null) return 1;
// On compare par nom (en respectant le standard de tri des strings)
return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
}
}
On utilise le comparateur pour trier la liste :
List<User> users = new List<User>
{
new User { Name = "Ivan", Age = 20, Email = "ivan@mail.com" },
new User { Name = "Anna", Age = 32, Email = "anna@gmail.com" },
new User { Name = "Boris", Age = 28, Email = "boris@work.org" },
new User { Name = "Ruslan", Age = 19, Email = "ruslan@yandex.ru" }
};
// On trie par nom avec IComparer
users.Sort(new UserNameComparer());
users.ForEach(u => Console.WriteLine(u.Name)); // Anna, Boris, Ivan, Ruslan
Tu vois comme c'est clean et élégant ? Le comparateur devient un citoyen à part entière de ton appli — tu peux l'utiliser pour n'importe quelle autre liste d'utilisateurs.
4. Plusieurs façons de comparer : on crée différents comparateurs
Ce qui est cool, c'est que tu peux créer autant de comparateurs que tu veux. Par exemple, on va coder un tri par âge :
// Comparateur pour trier par âge
public class UserAgeComparer : IComparer<User>
{
public int Compare(User x, User y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
// On trie par âge croissant
return x.Age.CompareTo(y.Age);
}
}
Et maintenant :
users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Affiche : Ruslan (19) Ivan (20) Boris (28) Anna (32)
Si tu veux trier par âge dans l'autre sens, tu peux juste inverser les arguments :
// Comparateur pour trier par âge décroissant
public class UserAgeDescendingComparer : IComparer<User>
{
public int Compare(User x, User y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
// On inverse l'ordre : y.CompareTo(x)
return y.Age.CompareTo(x.Age);
}
}
5. Quelques détails utiles
Comment ça marche sous le capot ?
Quand tu fais Sort() avec un comparateur, la liste passe chaque élément à ce comparateur et demande : "Qui je mets avant ?". Ton Compare répond : "celui-là, celui-là ou tu peux laisser comme ça". Le tri répète ce dialogue pour toutes les paires jusqu'à ce que la liste soit bien triée.
Que faire si les valeurs sont égales ? Tu retournes juste 0 — ça veut dire que l'ordre des éléments ne change pas (ou c'est décidé par les mécanismes internes du tri).
Où on utilise encore IComparer<T> ?
L'interface IComparer<T> est utilisée pas que dans les listes. Voilà quelques exemples où tu la croises dans .NET :
- Dans les constructeurs de collections comme SortedList<TKey, TValue> et SortedSet<T> : tu dois définir un ordre pour les éléments.
- Pour la recherche avec BinarySearch.
Exemple :
var sortedSet = new SortedSet<User>(new UserAgeComparer());
Maintenant SortedSet va toujours garder l'ordre par âge automatiquement !
Petit rappel sur la sécurité avec null
Une des erreurs les plus fréquentes chez les débutants, c'est NullReferenceException. Oublie pas de vérifier les objets null dans Compare, surtout si ta liste peut en contenir.
Le pattern classique (on le remet ici pour être sûr) :
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
C'est une bonne habitude : ça évite que ton appli plante au pire moment !
Avantages et limites de la méthode IComparer<T>
- Tu sépares bien la logique de comparaison des données. La classe User n'a pas à se soucier du pourquoi et du comment du tri.
- Tu peux réutiliser la logique de comparaison partout.
- C'est scalable : tu peux créer autant de tris différents que tu veux sans toucher au type de base.
Mais fais gaffe à pas oublier les null ou à avoir une logique de comparaison incohérente. Par exemple, si Compare(x, y) retourne 0, alors Compare(y, x) doit aussi retourner 0 ; si Compare(x, y) retourne >0, alors Compare(y, x) doit retourner <0, etc.
Petit mémo visuel : quand utiliser quoi ?
| Besoin | Quoi utiliser | Où mettre la logique |
|---|---|---|
| Un seul tri "naturel" | |
Dans le type lui-même (classe/struct) |
| Plusieurs façons de trier | |
Dans une classe séparée |
| Rapide, ponctuel, "à la volée" | / lambda |
En paramètre de la méthode Sort, via un délégué |
| Logique complexe et souvent utilisée | |
Comme classe comparateur séparée |
Dans la prochaine conf, tu verras comment utiliser et combiner les delegates et les expressions lambda pour comparer des objets. En attendant, essaie de coder plusieurs comparateurs pour ton appli et kiffe une archi élégante où le tri est en dehors de la classe principale, et où le choix du critère reste flexible et extensible.
6. Erreurs classiques quand on code des comparateurs
Erreur n°1 : pas de check sur null.
Si un des objets à comparer est null et que t'as pas prévu ça dans ton code, ton appli peut crasher avec une NullReferenceException.
Erreur n°2 : valeurs incorrectes pour -1, 0, +1.
La méthode Compare doit retourner un nombre négatif si le premier objet est plus petit, zéro s'ils sont égaux, et positif s'il est plus grand. Si tu respectes pas ça, le tri va faire des trucs bizarres.
Erreur n°3 : logique de comparaison asymétrique.
Si en comparant x et y tu retournes un truc, et en comparant y et x tu retournes la même chose (au lieu de l'opposé), le résultat sera imprévisible.
Erreur n°4 : utiliser Sort() sans comparateur pour un type custom.
Si ton type n'implémente pas IComparable ou IComparable<T>, appeler Sort() sans comparateur va lever une exception InvalidOperationException.
Comment éviter ça :
Teste les cas limites, couvre les parties critiques de ton code avec des tests unitaires (on en reparlera !), hésite pas à lire la doc — et que tes comparateurs tournent comme une horloge suisse.
GO TO FULL VERSION