web-dev-qa-db-fra.com

Consignes GetHashCode en C #

J'ai lu dans le livre Essential C # 3.0 et .NET 3.5 que:

Les retours de GetHashCode () pendant la durée de vie d'un objet particulier doivent être constants (la même valeur), même si les données de l'objet changent. Dans de nombreux cas, vous devez mettre en cache la méthode return pour appliquer cela.

Est-ce une directive valable?

J'ai essayé quelques types intégrés dans .NET et ils ne se sont pas comportés comme ça.

131
Joan Venge

La réponse est surtout, c'est une directive valide, mais peut-être pas une règle valide. Il ne raconte pas non plus toute l'histoire.

Le fait étant que pour les types mutables, vous ne pouvez pas baser le code de hachage sur les données mutables car deux objets égaux doivent retourner le même code de hachage et le code de hachage doit être valide pour la durée de vie de l'objet. Si le code de hachage change, vous vous retrouvez avec un objet qui se perd dans une collection de hachage car il ne vit plus dans la corbeille de hachage correcte.

Par exemple, l'objet A renvoie un hachage de 1. Ainsi, il va dans le bac 1 de la table de hachage. Ensuite, vous modifiez l'objet A de telle sorte qu'il renvoie un hachage de 2. Lorsqu'une table de hachage va le chercher, il regarde dans le bac 2 et ne le trouve pas - l'objet est orphelin dans le bac 1. C'est pourquoi le code de hachage doit ne change pas pour la durée de vie de l'objet, et juste une des raisons pour lesquelles l'écriture d'implémentations GetHashCode est une douleur dans le cul.

Mise à jour
Eric Lippert a publié un blog qui donne d'excellentes informations sur GetHashCode.

Mise à jour supplémentaire
J'ai apporté quelques modifications ci-dessus:

  1. J'ai fait une distinction entre directive et règle.
  2. J'ai barré "pour la durée de vie de l'objet".

Une ligne directrice n'est qu'un guide, pas une règle. En réalité, GetHashCode ne doit suivre ces directives que lorsque les choses s'attendent à ce que l'objet suive les directives, comme lorsqu'il est stocké dans une table de hachage. Si vous n'avez jamais l'intention d'utiliser vos objets dans des tables de hachage (ou tout autre élément qui repose sur les règles de GetHashCode), votre implémentation n'a pas besoin de suivre les instructions.

Lorsque vous voyez "pour la durée de vie de l'objet", vous devez lire "pendant le temps nécessaire à l'objet pour coopérer avec les tables de hachage" ou similaire. Comme la plupart des choses, GetHashCode consiste à savoir quand enfreindre les règles.

89
Jeff Yates

Cela fait longtemps, mais je pense néanmoins qu'il est encore nécessaire de donner une réponse correcte à cette question, y compris des explications sur le pourquoi et le comment. La meilleure réponse jusqu'à présent est celle citant le MSDN de manière exhaustive - n'essayez pas de créer vos propres règles, les gars de MS savaient ce qu'ils faisaient.

Mais tout d'abord: la directive citée dans la question est erronée.

Maintenant, pourquoi - il y en a deux

D'abord pourquoi: Si le hashcode est calculé d'une manière, qu'il ne change pas pendant la durée de vie d'un objet, même si l'objet lui-même change, alors il romprait le contrat égal.

Rappelez-vous: "Si deux objets se comparent comme égaux, la méthode GetHashCode pour chaque objet doit renvoyer la même valeur. Cependant, si deux objets ne se comparent pas comme égaux, les méthodes GetHashCode pour les deux objets ne doivent pas renvoyer des valeurs différentes."

La deuxième phrase est souvent mal interprétée comme "La seule règle est qu'au moment de la création de l'objet, le code de hachage des objets égaux doit être égal". Je ne sais pas vraiment pourquoi, mais c'est aussi l'essence de la plupart des réponses ici.

Pensez à deux objets contenant un nom, où le nom est utilisé dans la méthode equals: Même nom -> même chose. Créer l'instance A: Nom = Joe Créer l'instance B: Nom = Peter

Hashcode A et Hashcode B ne seront probablement pas les mêmes. Que se passerait-il maintenant, lorsque le nom de l'instance B est remplacé par Joe?

Selon la directive de la question, le code de hachage de B ne changerait pas. Le résultat serait: A.Equals (B) ==> true Mais en même temps: A.GetHashCode () == B.GetHashCode () ==> false.

Mais exactement ce comportement est interdit explicitement par le contrat equals & hashcode.

Deuxième pourquoi: Bien qu'il soit - bien sûr - vrai, que les changements dans le code de hachage pourraient casser les listes de hachage et d'autres objets en utilisant le code de hachage, l'inverse est également vrai. Le fait de ne pas modifier le code de hachage obtiendra dans le pire des cas des listes de hachage, où tous les objets différents auront le même code de hachage et seront donc dans le même bac de hachage - cela se produit lorsque les objets sont initialisés avec une valeur standard, par exemple.


Maintenant, venons-en aux hows Eh bien, à première vue, il semble y avoir une contradiction - de toute façon, le code va casser. Mais aucun problème ne vient d'un hashcode modifié ou inchangé.

La source des problèmes est bien décrite dans le MSDN:

De l'entrée de la table de hachage de MSDN:

Les objets clés doivent être immuables tant qu'ils sont utilisés comme clés dans la table de hachage.

Cela signifie:

Tout objet qui crée une valeur de hachage doit changer la valeur de hachage lorsque l'objet change, mais il ne doit pas - absolument ne doit pas - autoriser de modifications à lui-même, lorsqu'il est utilisé à l'intérieur d'une table de hachage (ou tout autre objet utilisant Hash, bien sûr) .

Tout d'abord, la manière la plus simple serait bien sûr de concevoir des objets immuables uniquement pour une utilisation dans des tables de hachage, qui seront créés en tant que copies des objets normaux, mutables en cas de besoin. À l'intérieur des objets immuables, il est évidemment correct de mettre en cache le code de hachage, car il est immuable.

Deuxièmement, comment donner à l'objet un drapeau "vous êtes haché maintenant", assurez-vous que toutes les données d'objet sont privées, vérifiez l'indicateur dans toutes les fonctions qui peuvent modifier les données des objets et lancez des données d'exception si le changement n'est pas autorisé (c'est-à-dire que l'indicateur est défini ). Maintenant, lorsque vous placez l'objet dans une zone hachée, assurez-vous de définir le drapeau et - également - de désactiver le drapeau, lorsqu'il n'est plus nécessaire. Pour faciliter l'utilisation, je vous conseille de définir le drapeau automatiquement dans la méthode "GetHashCode" - de cette façon, il ne peut pas être oublié. Et l'appel explicite d'une méthode "ResetHashFlag" s'assurera, que le programmeur devra penser, s'il est autorisé ou non à modifier les données des objets maintenant.

Ok, ce qu'il faut dire aussi: il y a des cas, où il est possible d'avoir des objets avec des données mutables, où le hashcode est néanmoins inchangé, lorsque les données des objets sont modifiées, sans violer le contrat égal et hashcode.

Cela nécessite cependant que la méthode d'égalité ne soit pas également basée sur les données mutables. Donc, si j'écris un objet et que je crée une méthode GetHashCode qui ne calcule une valeur qu'une seule fois et la stocke à l'intérieur de l'objet pour la renvoyer lors d'appels ultérieurs, alors je dois, encore une fois: absolument, créer une méthode Equals, qui utilisera valeurs stockées pour la comparaison, de sorte que A.Equals (B) ne passe jamais de faux à vrai également. Sinon, le contrat serait rompu. Le résultat de ceci sera généralement que la méthode Equals n'a aucun sens - ce n'est pas la référence d'origine égale, mais ce n'est pas non plus une valeur égale. Parfois, cela peut être un comportement voulu (c'est-à-dire des enregistrements client), mais ce n'est généralement pas le cas.

Donc, modifiez simplement le résultat GetHashCode lorsque les données d'objet changent et si l'utilisation de l'objet à l'intérieur du hachage à l'aide de listes ou d'objets est prévue (ou tout simplement possible), puis rendez l'objet immuable ou créez un indicateur en lecture seule à utiliser pour le durée de vie d'une liste hachée contenant l'objet.

(Soit dit en passant: tout cela n'est pas spécifique à C # oder .NET - c'est dans la nature de toutes les implémentations de table de hachage, ou plus généralement de toute liste indexée, que les données d'identification des objets ne doivent jamais changer, alors que l'objet est dans la liste . Un comportement inattendu et imprévisible se produira si cette règle est violée. Quelque part, il peut y avoir des implémentations de liste, qui surveillent tous les éléments de la liste et réindexent automatiquement la liste - mais les performances de ceux-ci seront sûrement horribles au mieux.)

119
Alex

De MSDN

Si deux objets se comparent comme égaux, la méthode GetHashCode pour chaque objet doit retourner la même valeur. Toutefois, si deux objets ne se comparent pas comme égaux, les méthodes GetHashCode pour les deux objets ne doivent pas renvoyer de valeurs différentes.

La méthode GetHashCode d'un objet doit renvoyer systématiquement le même code de hachage tant qu'il n'y a pas de modification de l'état de l'objet qui détermine la valeur de retour de la méthode Equals de l'objet. Notez que cela n'est vrai que pour l'exécution en cours d'une application et qu'un code de hachage différent peut être renvoyé si l'application est réexécutée.

Pour de meilleures performances, une fonction de hachage doit générer une distribution aléatoire pour toutes les entrées.

Cela signifie que si la ou les valeurs de l'objet changent, le code de hachage doit changer. Par exemple, une classe "Personne" avec la propriété "Nom" définie sur "Tom" doit avoir un code de hachage et un code différent si vous changez le nom en "Jerry". Sinon, Tom == Jerry, ce qui n'est probablement pas ce que vous auriez voulu.


Modifier :

Aussi à partir de MSDN:

Les classes dérivées qui remplacent GetHashCode doivent également remplacer Equals pour garantir que deux objets considérés comme égaux ont le même code de hachage; sinon, le type Hashtable peut ne pas fonctionner correctement.

De entrée de la table de hachage de MSDN :

Les objets clés doivent être immuables tant qu'ils sont utilisés comme clés dans la table de hachage.

La façon dont je lis ceci est que les objets mutables devraient renvoient différents codes de hachage lorsque leurs valeurs changent, à moins que ils sont conçus pour être utilisés dans une table de hachage.

Dans l'exemple de System.Drawing.Point, l'objet est modifiable et le fait renvoie un code de hachage différent lorsque la valeur X ou Y change. Cela en ferait un mauvais candidat à utiliser tel quel dans une table de hachage.

9
Jon B

Je pense que la documentation concernant GetHashcode est un peu déroutante.

D'une part, MSDN indique que le code de hachage d'un objet ne doit jamais changer et qu'il est constant.D'autre part, MSDN indique également que la valeur de retour de GetHashcode doit être égale pour 2 objets, si ces 2 objets sont considérés comme égaux.

MSDN:

Une fonction de hachage doit avoir les propriétés suivantes:

  • Si deux objets se comparent comme égaux, la méthode GetHashCode pour chaque objet doit retourner la même valeur. Toutefois, si deux objets ne se comparent pas comme égaux, les méthodes GetHashCode pour les deux objets ne doivent pas renvoyer de valeurs différentes.
  • La méthode GetHashCode d'un objet doit renvoyer systématiquement le même code de hachage tant qu'il n'y a pas de modification de l'état de l'objet qui détermine la valeur de retour de la méthode Equals de l'objet. Notez que cela n'est vrai que pour l'exécution en cours d'une application et qu'un code de hachage différent peut être renvoyé si l'application est réexécutée.
  • Pour de meilleures performances, une fonction de hachage doit générer une distribution aléatoire pour toutes les entrées.

Ensuite, cela signifie que tous vos objets doivent être immuables ou que la méthode GetHashcode doit être basée sur des propriétés immuables de votre objet. Supposons par exemple que vous ayez cette classe (implémentation naïve):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

Cette implémentation viole déjà les règles qui peuvent être trouvées dans MSDN. Supposons que vous ayez 2 instances de cette classe; la propriété Name de l'instance1 est définie sur "Pol" et la propriété Name de l'instance2 est définie sur "Piet". Les deux instances renvoient un code de hachage différent, et elles ne sont également pas égales. Supposons maintenant que je change le nom d'instance2 en 'Pol', puis, selon ma méthode Equals, les deux instances doivent être égales et selon l'une des règles de MSDN, elles doivent renvoyer le même code de hachage.
Cependant, cela ne peut pas être fait, car le code de hachage d'instance2 va changer et MSDN indique que cela n'est pas autorisé.

Ensuite, si vous avez une entité, vous pouvez peut-être implémenter le code de hachage afin qu'il utilise l'`` identifiant principal '' de cette entité, qui est peut-être idéalement une clé de substitution ou une propriété immuable. Si vous avez un objet de valeur, vous pouvez implémenter le Hashcode afin qu'il utilise les "propriétés" de cet objet de valeur. Ces propriétés constituent la "définition" de l'objet valeur. C'est bien sûr la nature d'un objet de valeur; vous n'êtes pas intéressé par son identité, mais plutôt par sa valeur.
Et, par conséquent, les objets de valeur doivent être immuables. (Tout comme ils sont dans le framework .NET, string, Date, etc ... sont tous des objets immuables).

Une autre chose qui vient à l'esprit:
Au cours de quelle 'session' (je ne sais pas vraiment comment je dois l'appeler), 'GetHashCode' devrait retourner une valeur constante. Supposons que vous ouvrez votre application, chargez une instance d'un objet hors de la base de données (une entité) et obtenez son code de hachage. Il renverra un certain nombre. Fermez l'application et chargez la même entité. Est-il nécessaire que le hashcode cette fois ait la même valeur que lorsque vous avez chargé l'entité la première fois? À mon humble avis, non.

9
Frederik Gheysels

C'est un bon conseil. Voici ce que Brian Pepin a à dire à ce sujet:

Cela m'a fait trébucher plus d'une fois: assurez-vous que GetHashCode renvoie toujours la même valeur pendant la durée de vie d'une instance. N'oubliez pas que les codes de hachage sont utilisés pour identifier les "compartiments" dans la plupart des implémentations de hachage. Si le "bucket" d'un objet change, une table de hachage peut ne pas trouver votre objet. Ces bogues peuvent être très difficiles à trouver, alors corrigez-les du premier coup.

8
Justin R.

Découvrez cet article de blog de Marc Brooks:

VTO, RTO et GetHashCode () - oh, mon Dieu!

Et puis consultez le post de suivi (ne peut pas lier car je suis nouveau, mais il y a un lien dans l'article initial) qui discute plus en détail et couvre quelques faiblesses mineures dans la mise en œuvre initiale.

C'était tout ce que je devais savoir sur la création d'une implémentation GetHashCode (), il fournit même un téléchargement de sa méthode avec quelques autres utilitaires, en bref.

5
Shaun

Ne répondant pas directement à votre question, mais - si vous utilisez Resharper, n'oubliez pas qu'il a une fonctionnalité qui génère une implémentation raisonnable de GetHashCode (ainsi que la méthode Equals) pour vous. Vous pouvez bien sûr spécifier quels membres de la classe seront pris en compte lors du calcul du hashcode.

5
petr k.

Le hashcode ne change jamais, mais il est également important de comprendre d'où vient le Hashcode.

Si votre objet utilise la sémantique des valeurs, c'est-à-dire que l'identité de l'objet est définie par ses valeurs (comme String, Color, toutes les structures). Si l'identité de votre objet est indépendante de toutes ses valeurs, le Hashcode est identifié par un sous-ensemble de ses valeurs. Par exemple, votre entrée StackOverflow est stockée quelque part dans une base de données. Si vous modifiez votre nom ou votre e-mail, votre entrée client reste la même, bien que certaines valeurs aient changé (en fin de compte, vous êtes généralement identifié par un long identifiant client #).

Bref:

Sémantique du type de valeur - Le Hashcode est défini par des valeurs Sémantique du type de référence - Le Hashcode est défini par un id

Je vous suggère de lire Domain Driven Design par Eric Evans, où il va dans les types entités vs valeurs (ce qui est plus ou moins ce que j'ai essayé de faire ci-dessus) si cela n'a toujours pas de sens.

4
DavidN
3
Ian Ringrose