CodeGym /Cours /C# SELF /Interface IComparable<T...

Interface IComparable<T>

C# SELF
Niveau 30 , Leçon 0
Disponible

1. Intro

Imagine que tu demandes à un pote de ranger des livres sur des étagères. Si ce sont des livres avec des numéros sur la tranche (1, 2, 3…), ton pote va gérer facile : "Ok, 1 avant 2, 2 avant 3". Ça, c’est l’ordre naturel des nombres (par ordre croissant). Pareil, si c’est des livres dont le titre commence par "A", "B", "C", il va les ranger par ordre alphabétique. C’est aussi l’ordre "naturel" pour les chaînes (par alphabet).

Mais si sur la tranche il n’y a que les noms de famille des auteurs ? Et tu demandes : "Range-les dans l’ordre". Ton pote va demander : "Quel ordre ? Par nom ? Par année de sortie ? Par nombre de pages ?" Et là, c’est le début des embrouilles. Pour tes entités uniques, il n’y a pas d’ordre évident naturel.

C’est pareil en prog. Quand tu veux trier une liste d’entiers List<int>, C# sait très bien faire. Pour List<string> aussi, il gère, en utilisant l’ordre lexicographique (alphabétique). Mais si tu as une List<Student>, où chaque Student est un objet avec nom, prénom, âge, id, et plein d’autres trucs, là C# est perdu. Il ne sait pas sur quel critère comparer deux étudiants. Par le nom ? Par l’ID ? Par la moyenne ? C’est exactement le souci qu’on a vu dans la conf "Tri des collections", quand List<T>.Sort() a balancé une erreur en essayant de trier une collection de types custom.

Pour résoudre ce casse-tête, il faut donner à C# une consigne claire : sur quoi comparer nos objets. Pour ça, il y a l’interface IComparable<T>.

2. Interface IComparable<T>

Donc, pour que nos objets "sachent" où ils se placent par rapport aux autres, on utilise l’interface IComparable<T>. C’est un contrat. Quand ta classe ou ta struct implémente cette interface, tu dis au compilateur : "Yo ! Mes objets peuvent être comparés entre eux, et voilà comment tu dois faire."

Comment ça marche ?

L’interface IComparable<T> définit une seule méthode :

public interface IComparable<in T>
{
    int CompareTo(T other);
}

Cette méthode prend un objet du type T (le même type que l’objet courant) et doit renvoyer :

  • un nombre négatif (< 0) si l’objet courant est "plus petit" que celui qu’on compare ;
  • zéro (0) s’ils sont "égaux" (pour le tri) ;
  • un nombre positif (> 0) si l’objet courant est "plus grand".

En gros, c’est comme des juges dans un match de boxe : si t’es meilleur, tu gagnes le round et tu prends plus de points ; si t’es moins bon — moins ; si c’est égal, même score. Sauf qu’ici, au lieu de juges, c’est juste un nombre avec un signe.

Pourquoi comme ça ?

Les méthodes de tri (genre List<T>.Sort()) appellent CompareTo sur les éléments de la liste pour savoir qui vient avant qui. Si ta classe implémente cette interface — elle est triable !

3. Pratique

Imaginons qu’on a une classe utilisateur (User) :

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Essayons de trier une liste d’utilisateurs :

List<User> users = new List<User>
{
    new User { Name = "Sergey", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

users.Sort(); // BOUM ! InvalidOperationException

Erreur qui pop : "At least one object must implement IComparable" (Au moins un objet doit implémenter IComparable).

On corrige : on implémente IComparable<User>

On ajoute l’interface à notre classe. Pour commencer, on trie par âge — du plus jeune au plus vieux :

public class User : IComparable<User>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public int CompareTo(User other)
    {
        // Sécurité : si other == null, notre user est "plus grand"
        if (other == null) return 1;
        return this.Age.CompareTo(other.Age); // tri par âge
    }
}

Maintenant, on fait une liste, on appelle users.Sort(); et on affiche le résultat dans la console :

List<User> users = new List<User>
{
    new User { Name = "Sergey", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

// Tri par âge (utilise CompareTo)
users.Sort();

// Affichage des users triés
foreach (User user in users)
{
    Console.WriteLine($"{user.Name}, {user.Age}");
}

La liste sera triée par âge :

Maria, 22
Anton, 27
Sergey, 31

Visualisation : avant/après

Nom Âge
Sergey 31
Maria 22
Anton 27

Après tri :

Nom Âge
Maria 22
Anton 27
Sergey 31

4. Détails importants et erreurs fréquentes

Sécurité contre null

Dans CompareTo, c’est super important de vérifier que other n’est pas null. Sinon, tu risques un NullReferenceException — et ça, c’est aussi traître qu’un bug la veille d’un déploiement. En général, si l’objet comparé est null, on considère que l’objet courant est "plus grand" :

public int CompareTo(User other)
{
    if (other == null) return 1;
    // ...
}

Respecte la transitivité

Si A < B et B < C, alors A doit être < C. Si tu ne respectes pas ça, le tri va partir en freestyle (donc fun, mais faux !).

Si tu veux trier sur plusieurs champs

Par exemple, d’abord par âge, mais si deux users ont le même âge, on les trie par nom (ordre alphabétique). Voilà comment faire :

public int CompareTo(User other)
{
    if (other == null) return 1;

    int ageCompare = this.Age.CompareTo(other.Age);
    if (ageCompare != 0) return ageCompare;

    // Si les âges sont égaux — on compare les noms
    return this.Name.CompareTo(other.Name);
}

5. Tri : maintenant avec tes objets

Tout ce que sait faire List<int> — ta classe sait le faire aussi

Maintenant tu peux utiliser n’importe quelle méthode qui a besoin de comparer : Sort, BinarySearch, même l’insertion dans des collections triées (genre SortedSet<T>).

users.Sort();
// users est maintenant trié par âge (et par nom si même âge)

Exemple dans une appli de cours

Imaginons que tu as déjà codé une appli de gestion d’utilisateurs. Maintenant, tu peux leur donner un ordre "naturel" de tri direct dans le code existant. Voilà à quoi ça ressemble :

// Notre classe User implémente déjà IComparable<User>

List<User> users = new List<User>
{
    new User { Name = "Ivan", Age = 45 },
    new User { Name = "Galina", Age = 27 },
    new User { Name = "Yuri", Age = 27 }
};

// On trie par âge, puis par nom
users.Sort();
foreach (var u in users)
{
    Console.WriteLine($"{u.Name} - {u.Age}");
}

Résultat :

Galina - 27
Yuri - 27
Ivan - 45

Galina et Yuri ont le même âge, donc c’est le nom qui décide.

6. Comment marche CompareTo : la math hardcore

Petit rappel sur le standard des retours :

  • Nombre négatif (genre -1) : l’objet courant vient avant l’autre.
  • Zéro : ils sont égaux pour le tri.
  • Nombre positif (genre 1) : l’objet courant vient après l’autre.

Les types natifs (genre Age.CompareTo(other.Age)) suivent déjà ce standard, ils renvoient toujours -1, 0 ou 1.

Tableau des retours pour la méthode CompareTo

Valeur retournée Ça veut dire quoi ? Exemple
< 0 Plus petit (vient avant)
20.CompareTo(21): -1
0 Égal
23.CompareTo(23): 0
> 0 Plus grand (vient après)
42.CompareTo(15): 1

7. Tri multiple : on combine les champs

Parfois, il faut un tri plus complexe : par nom de famille, prénom et âge. Avec la même technique, tu compares dans l’ordre :

public class Student : IComparable<Student>
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public int CompareTo(Student other)
    {
        if (other == null) return 1;
        int lastNameCompare = this.LastName.CompareTo(other.LastName);
        if (lastNameCompare != 0) return lastNameCompare;
        int firstNameCompare = this.FirstName.CompareTo(other.FirstName);
        if (firstNameCompare != 0) return firstNameCompare;
        return this.Grade.CompareTo(other.Grade);
    }
}

8. Quand il NE FAUT PAS implémenter IComparable<T>

Cas de la vraie vie (de dev !) : si ton objet n’a pas d’ordre de tri "naturel", mieux vaut ne PAS implémenter IComparable<T>. Par exemple, si tu as une classe Point, mais tu ne sais pas — trier par X, par Y ou par la distance à l’origine — alors laisse le tri venir de l’extérieur avec une fonction de comparaison (IComparer<T>). On en parlera à la prochaine conf.

2
Mission
C# SELF, niveau 30, leçon 0
Bloqué
Implémentation de l'interface IComparable pour comparer selon un seul champ
Implémentation de l'interface IComparable pour comparer selon un seul champ
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION