web-dev-qa-db-fra.com

Pourquoi est-il important de remplacer GetHashCode lorsque la méthode Equals est remplacée?

Étant donné la classe suivante

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null) 
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

J'ai remplacé la méthode Equals parce que Foo représente une ligne pour la table Foos. Quelle est la méthode préférée pour remplacer le GetHashCode?

Pourquoi est-il important de remplacer GetHashCode?

1344
David Basarab

Oui, il est important que votre élément soit utilisé comme clé dans un dictionnaire ou HashSet<T>, etc., car il est utilisé (en l’absence de IEqualityComparer<T> personnalisé) pour regrouper les éléments dans des compartiments. Si le code de hachage de deux éléments ne correspond pas, ils peuvent ne jamais être considérés comme égaux (Equals ne sera tout simplement jamais appelé).

La méthode GetHashCode() doit refléter la logique Equals; les règles sont:

  • si deux choses sont égales (Equals(...) == true), alors elles doivent renvoyer la même valeur pour GetHashCode()
  • si la GetHashCode() est égale, il est pas nécessaire pour qu'elles soient identiques; c'est une collision et Equals sera appelée pour voir si c'est une égalité réelle ou non.

Dans ce cas, il semble que "return FooId;" soit une implémentation appropriée de GetHashCode(). Si vous testez plusieurs propriétés, il est courant de les combiner avec le code suivant, afin de réduire les collisions diagonales (c’est-à-dire que new Foo(3,5) possède un code de hachage différent de new Foo(5,3)):

unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
    int hash = 13;
    hash = (hash * 7) + field1.GetHashCode();
    hash = (hash * 7) + field2.GetHashCode();
    ...
    return hash;
}

Oh, pour plus de commodité, vous pouvez également envisager de fournir des opérateurs == et != lors de la substitution de Equals et GetHashCode.


Une démonstration de ce qui se passe lorsque vous vous trompez est ici .

1238
Marc Gravell

Il est en fait très difficile d'implémenter correctement GetHashCode() car, en plus des règles déjà mentionnées par Marc, le code de hachage ne devrait pas changer pendant la durée de vie d'un objet. Par conséquent, les champs utilisés pour calculer le code de hachage doivent être immuables.

J'ai finalement trouvé une solution à ce problème lorsque je travaillais avec NHibernate. Mon approche consiste à calculer le code de hachage à partir de l'ID de l'objet. L'ID ne peut être défini que par le constructeur. Par conséquent, si vous souhaitez modifier l'ID, ce qui est très improbable, vous devez créer un nouvel objet doté d'un nouvel ID et donc d'un nouveau code de hachage. Cette approche fonctionne mieux avec les GUID car vous pouvez fournir un constructeur sans paramètre qui génère de manière aléatoire un ID.

129
Albic

En remplaçant Equals, vous indiquez en gros que vous êtes celui qui sait mieux comparer deux instances d'un type donné, de sorte que vous êtes probablement le meilleur candidat pour fournir le meilleur code de hachage.

Voici un exemple de la façon dont ReSharper écrit une fonction GetHashCode () pour vous:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

Comme vous pouvez le constater, il essaie simplement de deviner un bon code de hachage basé sur tous les champs de la classe, mais puisque vous connaissez le domaine ou les plages de valeurs de votre objet, vous pouvez toujours en fournir un meilleur.

53
Trap

N'oubliez pas de vérifier le paramètre obj par rapport à null lors du remplacement de Equals(). Et aussi comparer le type.

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

La raison en est: Equals doit renvoyer false lors de la comparaison avec null. Voir aussi http://msdn.Microsoft.com/en-us/library/bsc2ak47.aspx

40
huha

Que diriez-vous:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

En supposant que la performance ne soit pas un problème :)

31
Ludmil Tinkov

Nous avons deux problèmes à résoudre.

  1. Vous ne pouvez pas fournir de GetHashCode() sensible si vous pouvez modifier n'importe quel champ de l'objet. De plus, un objet ne sera JAMAIS utilisé dans une collection qui dépend de GetHashCode(). Ainsi, le coût de la mise en œuvre de GetHashCode() n'en vaut souvent pas la peine, ou ce n'est pas possible.

  2. Si quelqu'un place votre objet dans une collection qui appelle GetHashCode() et que vous avez surchargé Equals() sans aussi forcer GetHashCode() à se comporter correctement, cette personne peut passer des jours à rechercher le problème.

Donc par défaut je le fais.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null)
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}
12
Ian Ringrose

Juste pour ajouter les réponses ci-dessus:

Si vous ne remplacez pas Equals, le comportement par défaut consiste à comparer les références des objets. La même chose s'applique à hashcode - l'implémentation par défaut est généralement basée sur une adresse mémoire de la référence. Parce que vous avez remplacé Equals, cela signifie que le bon comportement consiste à comparer tout ce que vous avez implémenté sur Equals et non les références. Vous devez donc faire la même chose pour le hashcode.

Les clients de votre classe s'attendent à ce que le hashcode ait une logique similaire à la méthode equals. Par exemple, les méthodes linq qui utilisent un IEqualityComparer comparent d'abord les hashcodes. Pour exécuter, si nous n’implémentions pas hashcode, equal object aurait probablement des hashcodes différents (parce qu’ils avaient une adresse mémoire différente) et serait déterminé à tort comme non égal (Equals () ne sera même pas touché).

De plus, si ce n’est le problème que vous pourriez ne pas être en mesure de trouver votre objet si vous l’utilisiez dans un dictionnaire (parce qu’il a été inséré par un hashcode et que vous le recherchiez, le hashcode par défaut sera probablement différent et encore Equals () ne sera même pas appelé, comme l'explique Marc Gravell dans sa réponse, vous introduisez également une violation du dictionnaire ou du concept de hachage qui ne devrait pas autoriser des clés identiques - vous avez déjà déclaré que ces objets sont essentiellement les mêmes lorsque vous écrasez la valeur Egal. ne voulez pas que les deux clés soient différentes sur une structure de données supposant une clé unique, mais comme elles ont un code de hachage différent, la "même" clé sera insérée en tant que clé différente.

10
BornToCode

C'est parce que la structure requiert que deux objets identiques aient le même hashcode. Si vous substituez la méthode equals pour effectuer une comparaison spéciale de deux objets et que les deux objets sont considérés comme identiques par la méthode, le code de hachage des deux objets doit également être identique. (Dictionnaires et tables de hachage reposent sur ce principe).

10
kemiller2002

Le code de hachage est utilisé pour les collections basées sur le hachage telles que Dictionnaire, Hashtable, HashSet, etc. Le but de ce code est de trier très rapidement un objet spécifique en le plaçant dans un groupe spécifique (compartiment). Ce tri préalable aide énormément à trouver cet objet lorsque vous devez le récupérer à partir d'une collection de hachages, car le code doit rechercher votre objet dans un seul compartiment plutôt que dans tous les objets qu'il contient. La meilleure distribution des codes de hachage (meilleure unicité) la récupération plus rapide. Dans une situation idéale où chaque objet a un code de hachage unique, le trouver est une opération O(1). Dans la plupart des cas, il approche de O (1).

8
Maciej

Ce n'est pas forcément important. cela dépend de la taille de vos collections et de vos exigences en matière de performances, ainsi que du fait que votre classe soit utilisée ou non dans une bibliothèque où vous ne connaissez peut-être pas les exigences en matière de performances. Je sais souvent que les tailles de ma collection ne sont pas très grandes et que mon temps est plus précieux que quelques microsecondes de performances obtenues en créant un code de hachage parfait; donc (pour se débarrasser de l'avertissement gênant du compilateur), j'utilise simplement:

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(Bien sûr, je pourrais aussi utiliser un # pragma pour désactiver l’avertissement, mais je préfère cette façon.)

Lorsque vous êtes dans la position où vous faites avez besoin de la performance que tous les problèmes mentionnés par d’autres ici s'appliquent, bien sûr. Le plus important - sinon, vous obtiendrez des résultats erronés lors de la récupération d'éléments d'un ensemble de hachage ou d'un dictionnaire: le code de hachage ne doit pas varier en fonction de la durée de vie d'un objet (plus précisément, pendant le temps où le code de hachage est nécessaire, par exemple en étant une clé dans un dictionnaire): par exemple, ce qui suit est faux car Value est public et peut donc être modifié en externe pour la classe pendant la durée de vie de l'instance, donc vous ne devez pas l'utiliser comme base du code de hachage:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

D'autre part, si Value ne peut pas être modifié, vous pouvez utiliser:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }

6
ILoveFortran

D'après ce que j'ai compris, GetHashCode () d'origine renvoie l'adresse de mémoire de l'objet. Il est donc essentiel de le remplacer si vous souhaitez comparer deux objets différents.

EDITED: Cela était incorrect, la méthode GetHashCode () d'origine ne peut pas garantir l'égalité de 2 valeurs. Bien que les objets qui sont égaux renvoient le même code de hachage.

0
user2855602