CodeGym /Cours /C# SELF /Cas complexes de comparaison et meilleures pratiques

Cas complexes de comparaison et meilleures pratiques

C# SELF
Niveau 30 , Leçon 3
Disponible

1. Tri multi-niveaux (hiérarchique)

Pourquoi comparer c’est pas toujours simple ?

En entretien C#, on adore poser des questions sur le tri et la comparaison d’objets un peu relous. C’est pas juste pour faire genre, c’est parce que c’est un vrai souci pour tous les devs. Chercher des enregistrements en base, trier des vitrines ou des tableaux dans l’UI, gérer l’unicité dans les collections — tout ça dépend direct de la façon dont tu compares tes objets.

Comparer des nombres, easy. Mais si tu veux trier des users par nom de famille, puis par prénom, et qu’en plus certains champs peuvent être vides (null) ou que les strings sont dans différentes langues… Là, faut une vraie méthode.

Souvent, on te demande : d’abord trier par un critère, puis si égalité — par un autre, et encore un autre si toujours égal.

Exemple : User avec prénom, nom et date de naissance


public class User
{
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public DateTime BirthDate { get; set; }

    // Pour le style — affiche les infos du user
    public override string ToString()
        => $"{LastName} {FirstName} ({BirthDate:yyyy-MM-dd})";
}

Pourquoi une hiérarchie ?

Imagine, t’as une liste de users et tu veux les afficher par ordre alphabétique : d’abord par nom, puis par prénom. Si nom et prénom sont identiques — par date de naissance.

Logique de comparaison : "chaîne de responsabilité"

C’est comme pour classer les participants à une compète : d’abord par points, si égalité — par le temps, et si encore égalité — par l’alphabet. En code, c’est simple :


public class UserComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // On compare les noms
        int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);

        if (result != 0) return result; // Si différents — c’est bon, on sort

        // Si noms égaux — on compare les prénoms
        result = string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
        if (result != 0) return result;

        // Si noms et prénoms égaux — on compare les dates de naissance
        return x.BirthDate.CompareTo(y.BirthDate);
    }
}

Comment utiliser :


var users = new List<User>
{
    new User { FirstName = "Ivan", LastName = "Ivanov", BirthDate = new DateTime(1990, 1, 1) },
    new User { FirstName = "Petr", LastName = "Ivanov", BirthDate = new DateTime(1992, 5, 1) },
    new User { FirstName = "Anna", LastName = "Petrova", BirthDate = new DateTime(1985, 8, 30) }
};

users.Sort(new UserComparer());
users.ForEach(Console.WriteLine);
// Petrova Anna (1985-08-30)
// Ivanov Ivan (1990-01-01)
// Ivanov Petr (1992-05-01)

2. Comment comparer les strings ? Particularités culturelles

Comparer des strings : Ordinal, CurrentCulture, InvariantCulture

On dirait qu’une string, c’est une string, même en Afrique, mais c’est pas si simple ! Genre, le russe ё et е, l’allemand ss et ß, la casse...

En .NET, pour comparer des strings, t’as des règles spéciales via StringComparison. Ça peut changer le tri et la recherche.

Exemple de comparaison de strings :


// en allemand, ß est presque équivalent à ss
string a = "straße";
string b = "STRASSE";

bool eq1 = string.Equals(a, b, StringComparison.Ordinal); // false
bool eq2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // false
bool eq3 = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase); // true 

Dans les deux premiers cas, la comparaison est byte à byte — sans tenir compte de la culture ou de la langue, donc ß et SS sont différents. Mais dans le troisième, on utilise la culture courante (genre allemand), et la string est vue comme un natif la lirait : ß est pris comme ss, la casse est ignorée. Donc eq3 renvoie true.

Comment choisir la bonne façon de comparer ?

  • Ordinal — rapide, byte à byte, parfait pour les trucs techniques (genre comparer des IDs).
  • CurrentCulture / InvariantCulture — pour les textes utilisateurs, ça suit les règles de la langue de l’OS ou de la culture choisie.

Dans Sort, Compare et autres, pense à préciser le mode de comparaison :


string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)

Particularités du tri selon la langue

En russe, ё peut venir après е, ou être considéré pareil (ça dépend de la culture !). Donc, si tu fais un truc sérieux (genre un annuaire), demande au client ou au business analyst comment trier les lettres "spéciales".

3. Protection contre null : tous les objets sont pas bien élevés

Que faire si un champ est null ?

Dans la vraie vie, y’a toujours quelqu’un qui oublie de remplir un champ, et là, quand tu compares — boom ! — NullReferenceException. À toi d’anticiper.

Exemple sans protection contre null :


public int Compare(User x, User y)
{
    return x.LastName.CompareTo(y.LastName); // si LastName == null, ça plante !
}

Exemple avec protection (null est plus petit que tout non-null) :


public int Compare(User x, User y)
{
    // On utilise un comparateur spécial pour string qui gère les null
    int byLastName = Comparer<string>.Default.Compare(x.LastName, y.LastName);
    if (byLastName != 0) return byLastName;

    // et ainsi de suite...
}

Encore plus court :


public int Compare(User x, User y)
{
    return string.Compare(x?.LastName, y?.LastName, StringComparison.OrdinalIgnoreCase);
}

Les "null" au début ou à la fin ?

Tu peux faire en sorte que tous les users avec un nom "vide" soient au début ou à la fin de la liste — ça dépend du besoin.


public int Compare(User x, User y)
{
    if (x.LastName == null && y.LastName == null) return 0;
    if (x.LastName == null) return 1;   // null — à la fin
    if (y.LastName == null) return -1;  // null — à la fin
    return string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
}

4. Comparaison sur plusieurs critères

Erreur classique des débutants — faire de l’arithmétique au lieu d’une "chaîne de responsabilité" pour comparer, genre :


// Ne fais pas ça !
public int Compare(User x, User y)
{
    // Mauvais exemple
    return (x.Age - y.Age) + string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
}

Cette méthode ne garantit pas un tri correct : si la différence d’âge est -100, et que la comparaison des strings donne 1, le résultat sera -99, ce qui n’a rien à voir avec la logique attendue.

Il faut utiliser une séquence claire :
s’il y a déjà une différence — on la retourne, sinon on regarde le critère suivant.

5. Petites astuces utiles

Que faire si les objets sont égaux sur les critères principaux ?

Si jamais les objets sont "égaux", c’est important que l’algo de tri soit stable : il ne change pas l’ordre des éléments qui sont égaux selon la comparaison. Le List<T>.Sort() intégré n’est pas garanti stable. Si c’est crucial (genre pour trier des tableaux avec plusieurs niveaux de tri utilisateur), utilise les méthodes LINQ OrderBy/ThenBy — elles sont stables.

Comparer avec des champs optionnels/nullable

En .NET, tu peux avoir des modèles où un champ est, par exemple, DateTime? (Nullable<DateTime>) ou int?. La logique est simple : null est plus petit que non-null, ou l’inverse — à toi de voir. Tu peux utiliser les helpers de la lib standard :


int result = Nullable.Compare<DateTime>(u1.BirthDate, u2.BirthDate);

Comparer avec des règles en plus

Parfois, il faut que la comparaison tienne compte du "poids" d’un critère, genre les clients VIP passent toujours devant. Solution : ajoute un "flag VIP" au début du tri.


public int Compare(User x, User y)
{
    // Les VIP en premier
    int vipResult = y.IsVip.CompareTo(x.IsVip); // true = 1, false = 0; tri décroissant
    if (vipResult != 0) return vipResult;

    // Le reste — comme d’hab
    int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
    if (result != 0) return result;
    return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
}

6. Conseils et best practices

Vérifie toujours les null
Le C# moderne veut de plus en plus être "null-safe", mais le vieux code — pas du tout. Mets toujours des protections ou utilise les méthodes adaptées.

Évite les "nombres magiques"
N’écris pas return x.Field - y.Field; pour des champs où tu peux dépasser le type (overflow). Si c’est des long — tu risques des bugs.

Utilise StringComparison
Ne te fie pas au comportement par défaut des comparaisons de strings. Passe toujours StringComparison.OrdinalIgnoreCase ou ce qui va bien pour ton cas.

Sépare comparaison et égalité
Les interfaces IComparable<T> et IEqualityComparer<T> servent à des trucs différents. Pour le tri, prends des comparateurs, pour l’unicité — l’équivalence. Parfois, ça peut donner des résultats différents !

Ajoute des tests pour les cas chelous
Vérifie que le tri marche bien si les champs sont égaux, vides, ou si les strings sont de casse ou de langue différente.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION