Je suis tombé sur cela récemment, jusqu’à présent, j’ai eu le plaisir de remplacer l’opérateur d’égalité ( == ) et / ou la méthode Equals pour voir si deux types de références contenaient réellement les mêmes données (deux instances différentes qui se ressemblent).
Je l’utilise encore plus depuis que je m’intéresse de plus en plus aux tests automatisés (en comparant les données de référence / attendues à celles renvoyées).
En parcourant certaines des normes de codification de MSDN, je suis tombé sur un article qui le déconseille. Maintenant, je comprends pourquoi l’article dit ceci (parce qu’ils ne sont pas la même instance ) mais il ne répond pas à la question:
Merci beaucoup ^ _ ^
On dirait que j’ai mal lu une partie de la documentation (la journée a été longue).
Si vous implémentez des types de référence, vous devez envisager de substituer la méthode Equals sur un type de référence si votre type ressemble à un type de base tel qu’un point, une chaîne, un BigNumber, etc. La plupart des types de référence ne doivent pas surcharger l’opérateur d’ égalité , même s’ils remplacent Equals . Toutefois, si vous implémentez un type de référence destiné à avoir une sémantique de valeur, tel qu’un type de numéro complexe, vous devez remplacer l’opérateur d’égalité.
On dirait que vous codez en C #, qui a une méthode appelée Equals que votre classe devrait implémenter, si vous voulez comparer deux objects en utilisant une autre mésortingque que “sont ces deux pointeurs (parce que les descripteurs d’objects ne sont que ça, des pointeurs) la même adresse mémoire? “.
J’ai saisi un exemple de code ici :
class TwoDPoint : System.Object { public readonly int x, y; public TwoDPoint(int x, int y) //constructor { this.x = x; this.y = y; } public override bool Equals(System.Object obj) { // If parameter is null return false. if (obj == null) { return false; } // If parameter cannot be cast to Point return false. TwoDPoint p = obj as TwoDPoint; if ((System.Object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public bool Equals(TwoDPoint p) { // If parameter is null return false: if ((object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public override int GetHashCode() { return x ^ y; } }
Java a des mécanismes très similaires. La méthode equals () fait partie de la classe Object et votre classe la surcharge si vous voulez ce type de fonctionnalité.
La raison de la surcharge de ‘==’ peut être une mauvaise idée pour les objects. En général, vous voulez toujours pouvoir faire les comparaisons “sont-ce le même pointeur”. Celles-ci sont généralement utilisées, par exemple, pour insérer un élément dans une liste où aucun doublon n’est autorisé, et certains éléments de votre framework risquent de ne pas fonctionner si cet opérateur est surchargé de manière non standard.
L’implémentation de l’égalité dans .NET correctement, efficacement et sans duplication de code est difficile. Plus précisément, pour les types de référence avec une sémantique de valeur (c’est-à-dire des types immuables qui considèrent l’équivalence comme une égalité ), vous devez implémenter l’interface System.IEquatable
et implémenter toutes les différentes opérations ( Equals
, GetHashCode
et ==
GetHashCode
!=
) .
À titre d’exemple, voici une classe implémentant l’égalité des valeurs:
class Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x = 0, int y = 0) { X = x; Y = y; } public bool Equals(Point other) { if (other is null) return false; return X.Equals(other.X) && Y.Equals(other.Y); } public override bool Equals(object obj) => Equals(obj as Point); public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs); public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs); public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); }
Les seules parties mobiles du code ci-dessus sont les parties en gras: la deuxième ligne dans Equals(Point other)
et la méthode GetHashCode()
. L’autre code doit restr inchangé.
Pour les classes de référence qui ne représentent pas des valeurs immuables, n’implémentez pas les opérateurs ==
et !=
. Au lieu de cela, utilisez leur signification par défaut, qui consiste à comparer l’identité de l’object.
Le code assimile intentionnellement même les objects d’un type de classe dérivé. Souvent, cela peut ne pas être souhaitable car l’égalité entre la classe de base et les classes dérivées n’est pas bien définie. Malheureusement, .NET et les directives de codage ne sont pas très clairs ici. Le code créé par Resharper, publié dans une autre réponse , est susceptible de présenter un comportement indésirable dans de tels cas, car Equals(object x)
et Equals(SecurableResourcePermission x)
traiteront ce cas différemment.
Pour modifier ce comportement, une vérification de type supplémentaire doit être insérée dans la méthode Equals
fortement typée ci-dessus:
public bool Equals(Point other) { if (other is null) return false; if (other.GetType() != GetType()) return false; return X.Equals(other.X) && Y.Equals(other.Y); }
Ci-dessous, j’ai résumé ce que vous devez faire lors de la mise en œuvre d’IEquatable et fourni la justification à partir des différentes pages de documentation MSDN.
IEquatable
L’interface System.IEquatable est utilisée pour comparer deux instances d’un object pour l’égalité. Les objects sont comparés en fonction de la logique implémentée dans la classe. La comparaison donne une valeur booléenne indiquant si les objects sont différents. Cela contraste avec l’interface System.IComparable, qui renvoie un entier indiquant comment les valeurs de l’object sont différentes.
L’interface IEquatable déclare deux méthodes qui doivent être remplacées. La méthode Equals contient l’implémentation pour effectuer la comparaison réelle et retourner true si les valeurs de l’object sont égales ou false si elles ne le sont pas. La méthode GetHashCode doit renvoyer une valeur de hachage unique pouvant être utilisée pour identifier de manière unique des objects identiques contenant des valeurs différentes. Le type d’algorithme de hachage utilisé est spécifique à l’implémentation.
Méthode IEquatable.Equals
- Vous devez implémenter IEquatable pour que vos objects gèrent la possibilité qu’ils soient stockés dans un tableau ou une collection générique.
- Si vous implémentez IEquatable, vous devez également remplacer les implémentations de classe de base de Object.Equals (Object) et GetHashCode afin que leur comportement soit cohérent avec celui de la méthode IEquatable.Equals
Directives pour remplacer Equals () et Operator == (Guide de programmation C #)
- x.Equals (x) renvoie true.
- x.Equals (y) renvoie la même valeur que y.Equals (x)
- if (x.Equals (y) && y.Equals (z)) renvoie true, alors x.Equals (z) renvoie true.
- Invocations successives de x. Equals (y) renvoie la même valeur tant que les objects référencés par x et y ne sont pas modifiés.
- X. Equals (null) renvoie false (pour les types de valeur non nullables uniquement. Pour plus d’informations, voir Types Nullables (Guide de programmation C #) .)
- La nouvelle implémentation de Equals ne devrait pas lancer d’exceptions.
- Il est recommandé que toute classe qui remplace Equals remplace également Object.GetHashCode.
- Il est recommandé, en plus de mettre en œuvre Equals (object), que toute classe implémente également Equals (type) pour leur propre type, afin d’améliorer les performances.
Par défaut, l’opérateur == teste l’égalité de référence en déterminant si deux références indiquent le même object. Par conséquent, les types de référence ne doivent pas implémenter operator == pour obtenir cette fonctionnalité. Lorsqu’un type est immuable, c’est-à-dire que les données contenues dans l’instance ne peuvent pas être modifiées, une surcharge de l’opérateur == pour comparer l’égalité des valeurs et une égalité de référence peut être utile car, en tant qu’objects immuables, elles peuvent être considérées comme longues comme ils ont la même valeur. Ce n’est pas une bonne idée de remplacer l’opérateur == dans les types non immuables.
- Les implémentations des opérateurs == surchargées ne doivent pas générer d’exceptions.
- Tout type qui surcharge l’opérateur == devrait également surcharger l’opérateur! =.
== Opérateur (Référence C #)
- Pour les types de valeur prédéfinis, l’opérateur d’égalité (==) renvoie true si les valeurs de ses opérandes sont égales, false sinon.
- Pour les types de référence autres que ssortingng, == renvoie true si ses deux opérandes font référence au même object.
- Pour le type de chaîne, == compare les valeurs des chaînes.
- Lorsque vous testez null en utilisant des comparaisons == dans votre opérateur == overrides, assurez-vous d’utiliser l’opérateur de classe d’object de base. Si vous ne le faites pas, une récursion infinie se produira résultant en un stackoverflow.
Méthode Object.Equals (Object)
Si votre langage de programmation prend en charge la surcharge d’opérateur et si vous choisissez de surcharger l’opérateur d’égalité pour un type donné, ce type doit remplacer la méthode Equals. De telles implémentations de la méthode Equals doivent renvoyer les mêmes résultats que l’opérateur d’égalité
Les instructions suivantes concernent l’implémentation d’un type de valeur :
- Pensez à substituer Equals pour obtenir des performances supérieures à celles fournies par l’implémentation par défaut de Equals on ValueType.
- Si vous remplacez Equals et que la langue prend en charge la surcharge d’opérateur, vous devez surcharger l’opérateur d’égalité pour votre type de valeur.
Les instructions suivantes sont destinées à implémenter un type de référence :
- Envisagez de remplacer Equals sur un type de référence si la sémantique du type est basée sur le fait que le type représente une ou plusieurs valeurs.
- La plupart des types de référence ne doivent pas surcharger l’opérateur d’égalité, même s’ils remplacent Equals. Toutefois, si vous implémentez un type de référence destiné à avoir une sémantique de valeur, tel qu’un type de numéro complexe, vous devez remplacer l’opérateur d’égalité.
Cet article recommande simplement de ne pas remplacer l’opérateur d’égalité (pour les types de référence), et non contre le remplacement de Equals. Vous devez remplacer Equals dans votre object (référence ou valeur) si les vérifications d’égalité signifient quelque chose de plus que les vérifications de référence. Si vous voulez une interface, vous pouvez également implémenter IEquatable (utilisé par les collections génériques). Si vous implémentez IEquatable, cependant, vous devez également remplacer les équations, comme l’indique la section Remarques de IEquatable:
Si vous implémentez IEquatable
, vous devez également remplacer les implémentations de classe de base de Object.Equals (Object) et GetHashCode afin que leur comportement soit cohérent avec celui de la méthode IEquatable .Equals. Si vous substituez Object.Equals (Object), votre implémentation remplacée est également appelée dans les appels à la méthode statique Equals (System.Object, System.Object) sur votre classe. Cela garantit que tous les appels de la méthode Equals renvoient des résultats cohérents.
En ce qui concerne si vous devez implémenter Equals et / ou l’opérateur d’égalité:
De la mise en œuvre de la méthode égale
La plupart des types de référence ne doivent pas surcharger l’opérateur d’égalité, même s’ils remplacent Equals.
À partir des lignes direcsortingces pour la mise en œuvre d’égaux et de l’opérateur d’égalité (==)
Remplacez la méthode Equals chaque fois que vous implémentez l’opérateur d’égalité (==), et faites-les faire la même chose.
Cela indique uniquement que vous devez remplacer Equals chaque fois que vous implémentez l’opérateur d’égalité. Cela ne signifie pas que vous devez remplacer l’opérateur d’égalité lorsque vous remplacez Equals.
Pour les objects complexes qui donneront des comparaisons spécifiques, alors l’implémentation d’IComparable et la définition de la comparaison dans les méthodes de comparaison sont une bonne implémentation.
Par exemple, nous avons des objects “Véhicule” où la seule différence peut être le numéro d’enregistrement et nous l’utilisons pour comparer afin de nous assurer que la valeur attendue lors du test est celle que nous souhaitons.
J’ai tendance à utiliser ce que fait automatiquement Resharper. Par exemple, il l’a créé automatiquement pour l’un de mes types de référence:
public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj); } public bool Equals(SecurableResourcePermission obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny); } public override int GetHashCode() { unchecked { int result = (int)ResourceUid; result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0); result = (result * 397) ^ AllowDeny.GetHashCode(); return result; } }
Si vous voulez remplacer ==
et toujours effectuer des vérifications de référence, vous pouvez toujours utiliser Object.ReferenceEquals
.
Microsoft semble avoir changé de réglage, ou du moins il y a des informations contradictoires sur le fait de ne pas surcharger l’opérateur d’égalité. Selon cet article de Microsoft intitulé Comment: définir l’égalité des valeurs pour un type:
“Les opérateurs == et! = Peuvent être utilisés avec des classes même si la classe ne les surcharge pas. Cependant, le comportement par défaut consiste à effectuer une vérification d’égalité de référence. Dans une classe, si vous surchargez la méthode Equals, vous devez surcharger la classe == et! = opérateurs, mais ce n’est pas obligatoire. ”
Selon Eric Lippert, dans sa réponse à une question que j’ai posée sur le code minimal pour l’égalité en C # , il dit:
“Le danger que vous rencontrez ici est que vous ayez un opérateur == défini pour vous qui référence l’égalité par défaut. Vous pourriez facilement vous retrouver dans une situation où une méthode Equals surchargée valorise l’égalité et == fait référence à l’égalité, puis vous utilisez accidentellement une égalité de référence sur des choses égales à la valeur qui ne sont pas des références.Ceci est une pratique sujette aux erreurs qui est difficile à repérer par une revue de code humain.
Il y a quelques années, j’ai travaillé sur un algorithme d’parsing statique pour détecter statistiquement cette situation, et nous avons trouvé un taux de défauts d’environ deux instances par million de lignes de code dans tous les codes étudiés. En considérant uniquement les codes de base qui avaient été annulés quelque part Equals, le taux de défauts était évidemment considérablement plus élevé!
De plus, considérez les coûts par rapport aux risques. Si vous avez déjà des implémentations d’IComparable, l’écriture de tous les opérateurs est sortingviale et ne comportera aucun bogue et ne sera jamais modifiée. C’est le code le moins cher que vous puissiez écrire. Si on a le choix entre le coût fixe de l’écriture et du test d’une douzaine de méthodes minuscules et le coût illimité de la recherche et de la correction d’un bogue difficile à voir avec une égalité de valeur, je sais laquelle choisir. ”
Le .NET Framework n’utilisera jamais == ou! = Avec aucun type que vous écrivez. Mais le danger est ce qui arriverait si quelqu’un d’autre le faisait. Donc, si la classe est pour un tiers, alors je fournirais toujours les opérateurs == et! =. Si la classe est uniquement destinée à être utilisée en interne par le groupe, j’implémenterais probablement encore les opérateurs == et! =.
Je n’implémenterais que les opérateurs <, <=,> et> = si IComparable a été implémenté. IComparable ne devrait être implémenté que si le type doit prendre en charge le classement – comme lors du sorting ou de l’utilisation dans un conteneur générique ordonné tel que SortedSet.
Si le groupe ou l’entreprise avait une politique en place pour ne jamais implémenter les opérateurs == et! =, Alors je suivrais bien sûr cette politique. Si une telle stratégie était en place, il serait judicieux de l’appliquer avec un outil d’parsing de code Q / A qui marque toute occurrence des opérateurs == et! = Lorsqu’il est utilisé avec un type de référence.
Je pense que l’obtention de quelque chose d’aussi simple que la vérification des objects pour une égalité correcte est un peu délicate avec la conception de .NET.
Pour Struct
1) Implémenter IEquatable
. Il améliore sensiblement les performances.
2) Étant donné que vous avez maintenant vos propres Equals
, remplacez GetHashCode
et soyez cohérent avec divers object.Equals
substitution de vérification d’égalité.
3) Surcharger ==
et !=
opérateurs n’ont pas besoin d’être religieusement car le compilateur avertira si vous assimilez involontairement une structure à une autre avec un ==
ou !=
, Mais il est bon de le faire pour être compatible avec les méthodes Equals
.
public struct Entity : IEquatable { public bool Equals(Entity other) { throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { if (obj == null || !(obj is Entity)) return false; return Equals((Entity)obj); } public static bool operator ==(Entity e1, Entity e2) { return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } }
Pour la classe
De MS:
La plupart des types de référence ne doivent pas surcharger l’opérateur d’égalité, même s’ils remplacent Equals.
Pour moi, ==
se sent comme une égalité de valeur, plus comme un sucre syntaxique pour la méthode Equals
. Ecrire a == b
est beaucoup plus intuitif que d’écrire a.Equals(b)
. Nous aurons rarement besoin de vérifier l’égalité de référence. Dans les niveaux abstraits traitant des représentations logiques d’objects physiques, ce n’est pas quelque chose que nous devrions vérifier. Je pense qu’avoir une sémantique différente pour ==
et Equals
peut être déroutant. Je crois que cela aurait dû être ==
pour l’égalité de valeur et l’égalité pour la référence (ou un meilleur nom comme IsSameAs
) en premier lieu. J’aimerais bien ne pas prendre la directive MS au sérieux ici, pas seulement parce que ce n’est pas naturel pour moi, mais aussi parce que la surcharge ==
ne cause aucun dommage majeur. Ce n’est pas différent de ne pas substituer les Equals
ou GetHashCode
non-génériques qui peuvent rogner, parce que framework n’utilise pas ==
que si nous l’utilisons nous-mêmes. Le seul avantage réel que je tire de ne pas surcharger ==
et !=
Sera la cohérence avec la conception de la structure entière sur laquelle je n’ai aucun contrôle. Et c’est vraiment une grande chose, alors malheureusement je vais m’en tenir à cela .
Avec sémantique de référence (objects mutables)
1) Remplacer Equals
et GetHashCode
.
2) L’implémentation d’ IEquatable
n’est pas une nécessité, mais ce sera bien si vous en avez un.
public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classes, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } }
Avec sémantique de valeur (objects immuables)
C’est la partie délicate. Peut être facilement gâché s’il n’est pas pris en charge ..
1) Remplacer Equals
et GetHashCode
.
2) surcharge ==
et !=
Pour correspondre à Equals
. Assurez-vous que cela fonctionne pour les valeurs NULL .
2) L’implémentation d’ IEquatable
n’est pas une nécessité, mais ce sera bien si vous en avez un.
public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classes, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public static bool operator ==(Entity e1, Entity e2) { if (ReferenceEquals(e1, null)) return ReferenceEquals(e2, null); return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } }
Prenez un soin particulier pour voir si cela peut fonctionner si votre classe peut être héritée, dans ce cas, vous devrez déterminer si un object de classe de base peut être égal à un object de classe dérivé. Idéalement, si aucun object de classe dérivée n’est utilisé pour la vérification d’égalité, une instance de classe de base peut être égale à une instance de classe dérivée et, dans ce cas, il n’est pas nécessaire de vérifier l’égalité de Type
dans les Equals
génériques de la classe de base.
En général, veillez à ne pas dupliquer le code. J’aurais pu IEqualizable
classe de base abstraite générique ( IEqualizable
ou plus) comme modèle pour permettre la réutilisation plus facile, mais malheureusement en C # qui m’empêche de dériver de classes supplémentaires.
Il est remarquable de voir à quel point il est difficile de faire les choses correctement…
La recommandation de Microsoft d’avoir Equals et == faire des choses différentes dans ce cas n’a pas de sens pour moi. À un moment donné, quelqu’un s’attend (à juste titre) à ce que Equals et == produisent le même résultat et que le code bombe.
Je cherchais une solution qui:
Je suis venu avec ceci:
class MyClass : IEquatable { public int X { get; } public int Y { get; } public MyClass(int x = 0, int y = 0) { X = x; Y = y; } public override bool Equals(object obj) { var o = obj as MyClass; return o is null ? false : X.Equals(oX) && Y.Equals(oY); } public bool Equals(MyClass o) => object.Equals(this, o); public static bool operator ==(MyClass o1, MyClass o2) => object.Equals(o1, o2); public static bool operator !=(MyClass o1, MyClass o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(X, Y); }
Ici, tout se retrouve dans Equals(object)
qui est toujours polymorphe, les deux cibles étant atteintes.
Dérivez comme ceci:
class MyDerived : MyClass, IEquatable { public int Z { get; } public int K { get; } public MyDerived(int x = 0, int y = 0, int z=0, int k=0) : base(x, y) { Z = z; K = k; } public override bool Equals(object obj) { var o = obj as MyDerived; return o is null ? false : base.Equals(obj) && Z.Equals(oZ) && K.Equals(oK); } public bool Equals(MyDerived other) => object.Equals(this, o); public static bool operator ==(MyDerived o1, MyDerived o2) => object.Equals(o1, o2); public static bool operator !=(MyDerived o1, MyDerived o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K); }
Ce qui est fondamentalement le même sauf pour un gotcha – quand Equals(object)
veut appeler base.Equals
attention à appeler base.Equals(object)
et non base.Equals(MyClass)
(ce qui provoquera une récursion sans fin).
Une mise en garde est que Equals(MyClass)
fera de la boxe dans cette implémentation, mais la boxe / unboxing est hautement optimisée et cela vaut la peine d’atteindre les objectives ci-dessus.
démo: https://dotnetfiddle.net/cCx8WZ
(Notez que c’est pour C #> 7.0)
(basé sur la réponse de Konard)