[.NET] Attention à la redéfinition de la méthode GetHashCode()

Aujourd’hui, un développeur d’un client m’a signaler un « bug » dans la classe Dictionary<TKey, TValue> du .NET Framework. Après avoir étudié son problème pendant plus de 2 heures, (et je ne vous parle pas de mes cheveux !), le bug du Dictionary<TKey, TValue> venait du fait qu’il avait redéfini la méthode GetHashCode() sur un objet qui se basait sur des champs non-immuables !

Certaines personnes doivent se poser la question suivante : « Pourquoi le calcul effectué dans la méthode GetHashCode() doit se baser sur des champs immuables » ? Et bien, pour être tout à fait exacte, la valeur retournée par la méthode GetHashCode() doit rester constante durant toute la vie de l’objet. Pourquoi ? Tout simplement parce que certaines classes font appel à cette méthode et stockent la valeur dans des champs internes. Si la valeur du hash-code de l’objet change, alors les valeurs précédemment stockées dans une variable ne seront en aucun cas mis à jour.

Le plus souvent lorsque j’explique à un développeur les bonnes pratiques de la réimplémentation de la méthode GetHashCode(), il ne m’écoute guerre… Voici donc un bout de code qui devrait vous prouver les dangers de redéfinir la méthode GetHashCode() sur des valeurs qui peuvent changer durant la vie d’un objet :

public static class Programme
{
    public static void Main(string[] args)
    {
        // Création d'un personne "DUPOND"
        Personne personne;
        personne = new Personne();
        personne.Nom = "DUPOND";

        // Création d'un dictionnaire Personne/Age
        Dictionary<Personne, int> personnes;
        personnes = new Dictionary<Personne, int>();
        personnes.Add(personne, 38);

        // Est-ce que la personne existe dans le dictionnaire ?
        Console.WriteLine(personnes.ContainsKey(personne));

        // Change du nom de la personne
        personne.Nom = "DURAND";

        // Est-ce que l'instance de la personne existe dans le dictionnaire ?
        Console.WriteLine(personnes.ContainsKey(personne));
    }
}

public class Personne
{
    public string Nom
    {
        get;
        set;
    }

    public override int GetHashCode()
    {
        return this.Nom.GetHashCode();
    }
}

Essayez de récupérer cet l’exemple et de le compiler et vous verrez !

Que se passe-t-il exactement ? Et bien lors de l’ajout de la personne dans l’objet Dictionary<TKey, TValue>, ce dernier stocke dans une variable interne la valeur retournée par la méthode GetHashCode() de la personne. Le Dictionary<TKey, TValue> stocke ces valeurs de hash-code afin d’éviter d’appeler plusieurs fois le GetHashCode() qui pourrait prendre beaucoup de temps pour réaliser des calculs. Au niveau de l’utilisation de ces hash-code, cela va permettre au Dictionary<TKey, TValue> de comparer les clés très rapidement avec des entiers au lieu des critères plus ou moins complexes (qui prendrait forcement beaucoup plus de temps…).

Dans notre cas, le Dictionary<TKey, TValue> a stocké en interne la valeur « 125809318 » qui correspond à la valeur de hash-code de la chaîne « DUPOND ». Nous changeons ensuite le nom de la personne et appelons la méthode ContainsKey(). Cette dernière appelle la méthode GetHashCode() sur la personne spécifiée en paramètre, qui cette fois ci va retourner « -801977402 » (c’est le hash-code du nom « DURAND »). La méthode ContainsKey() va ensuite comparer le hash-code obtenu précédemment avec ceux stockés en interne. Et donc bien évidemment, il ne trouvera pas le hash-code et donc notre objet ! Et pourtant notre objet existe bel et bien dans notre Dictionary<TKey, TValue>…

Si vous doutez, exécutez le code suivant :

// Récupérer la première clé
Personne p;
p = personnes.Keys.ElementAt(0);

// Contrôler si "personne" et "p" font référence au même objet
Console.WriteLine(object.ReferenceEquals(p, personne));

Vous verrez que le .NET Framework vous confirmera que l’objet dans le Dictionary<TKey, TValue> est bien notre personne.

Au passage, certains développeurs têtus me lancent comme argument stupide, qu’ils n’utilisent jamais de Dictionary<TKey, TValue> et donc il n’y aura aucun problème avec la méthode GetHashCode()… Et bien cette méthode est utilisée dans beaucoup d’autres classes du .NET Framework ! Donc ATTENTION !!!

La morale de cette histoire c’est qu’il faut se débrouiller par n’importe quel moyen afin que la valeur GetHashCode() retournée soit constante durant TOUTE LA VIE de l’objet. Le plus souvent on essayera de calculer le hash-code sur des valeurs immuables tel qu’un numéro de sécurité sociale pour des personnnes, un numéro de tatouage pour un p’tit toutou,…

Publié dans la catégorie .NET Framework, C#.
Tags : , , , , . TrackBack URL.

Leave a comment