Qu'est-ce qu'une "meilleure pratique" & # 8221; Pour comparer deux instances d'un type de référence?

StackOverflow https://stackoverflow.com/questions/104158

Question

Je suis tombé sur cette information récemment. Jusqu'à présent, je remplaçais heureusement l'opérateur d'égalité ( == ) et / ou la égal à afin de voir si deux références les types contenaient en fait les mêmes données (c.-à-d. deux instances différentes qui se ressemblent).

Je l'utilise encore plus depuis les tests automatisés (comparant les données de référence / attendues à celles renvoyées).

En parcourant certaines des normes de codage dans MSDN Je suis tombé sur un article qui déconseille. Maintenant, je comprends pourquoi l'article dit ceci (car ils ne sont pas la même instance ), mais il ne répond pas à la question suivante:

  1. Quel est le meilleur moyen de comparer deux types de référence?
  2. Devrions-nous implémenter IComparable ? (J'ai également vu mentionner que cela devrait être réservé aux types de valeur).
  3. Existe-t-il une interface que je ne connais pas?
  4. Devrions-nous simplement rouler nous-mêmes?!

Merci beaucoup ^ _ ^

Mettre à jour

On dirait que j'ai mal lu une partie de la documentation (la journée a été longue) et que j'ai ignoré est égal à peut être le chemin à parcourir ..

  

Si vous implémentez une référence   types, vous devriez envisager de remplacer   la méthode Equals sur un type de référence   si votre type ressemble à un type de base   tels que Point, String, BigNumber,   etc. La plupart des types de référence devraient   ne surchargez pas l'opérateur égalité ,   Même s'ils remplacent Equals . cependant,   si vous implémentez une référence   type qui est destiné à avoir une valeur   la sémantique, telle qu'un nombre complexe   tapez, vous devez remplacer l'égalité   opérateur.

Était-ce utile?

La solution

On dirait que vous codez en C #, avec une méthode appelée Equals que votre classe doit implémenter, si vous souhaitez comparer deux objets en utilisant une autre métrique que "sont ces deux pointeurs (car les descripteurs d'objet ne sont que , pointeurs) vers la même adresse mémoire? ".

J'ai récupéré un exemple de code dans 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 == p.x) && (y == p.y);
    }

    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 == p.x) && (y == p.y);
    }

    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 souhaitez ce type de fonctionnalité.

La raison pour laquelle surcharger "==" peut être une mauvaise idée pour les objets est que, généralement, vous souhaitez toujours pouvoir utiliser le "même pointeur". comparaisons. 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 infrastructure risquent de ne pas fonctionner si cet opérateur est surchargé de manière non standard.

Autres conseils

Il est difficile de mettre en œuvre l'égalité correctement dans .NET et sans duplication de code . Spécifiquement, pour les types de référence avec une sémantique de valeur (c.-à-d. types immuables qui traitent l'équvialence comme une égalité ), vous devez implémenter l'interface System.IEquatable < T > , et vous devez implémenter toutes les opérations ( est égal à , GetHashCode et == , ! = ).

A titre d'exemple, voici une classe implémentant l'égalité des valeurs:

class Point : IEquatable<Point> {
    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 de est égale à (Point other) et la méthode GetHashCode () . L’autre code devrait rester inchangé.

Pour les classes de référence qui ne représentent pas des valeurs immuables, n'implémentez pas les opérateurs == et ! = . Utilisez plutôt leur signification par défaut, à savoir comparer l'identité de l'objet.

Le code intentionnellement assimile même les objets 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 instructions de codage ne sont pas très clairs ici. Le code créé par Resharper, publié dans dans une autre réponse , peut donner lieu à un comportement indésirable dans de tels cas, car est égal à (objet x) et est égal à (SecurableResourcePermission x) traitera ce cas différemment.

Pour modifier ce comportement, une vérification de type supplémentaire doit être insérée dans la méthode 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);
}

J'ai résumé ci-dessous ce que vous devez faire lorsque vous implémentez IEquatable et fourni la justification à partir des différentes pages de documentation MSDN.

Résumé

  • Lorsque vous souhaitez tester l'égalité des valeurs (par exemple, lorsque vous utilisez des objets dans des collections), vous devez implémenter l'interface IEquatable, remplacer Object.Equals et GetHashCode pour votre classe.
  • Lorsque vous souhaitez tester l'égalité de référence, vous devez utiliser l'opérateur ==, l'opérateur! = et Object.ReferenceEquals .
  • Vous devez uniquement remplacer opérateur == et opérateur! = pour ValueTypes et types de référence immuables.

Justification

IEquatable

>
  

L’interface System.IEquatable est utilisée pour comparer deux instances d’un objet à l’égalité. Les objets sont comparés en fonction de la logique implémentée dans la classe. La comparaison aboutit à une valeur booléenne indiquant si les objets sont différents. Cela contraste avec l'interface System.IComparable, qui renvoie un entier indiquant en quoi les valeurs de l'objet sont différentes.

     

L’interface IEquatable déclare deux méthodes qui doivent être remplacées. La méthode Equals contient l'implémentation permettant d'effectuer la comparaison réelle et de renvoyer true si les valeurs de l'objet 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 objets 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 objets 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 redéfinir 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
  •   

Instructions pour remplacer Equals () et Operator == (C # Guide de programmation)

  
      
  • 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 objets référencés par x et y ne sont pas modifiés.
  •   
  • x. Equals (null) renvoie false (pour les types de valeur non nullable uniquement. Pour plus d'informations, voir Types nullables (Guide de programmation C #) .)
  •   
  • La nouvelle implémentation d'Equals ne devrait pas générer d'exceptions.
  •   
  • Il est recommandé que toute classe remplaçant Equals substitue également Object.GetHashCode.
  •   
  • Il est recommandé qu'en plus d'implémenter Equals (objet), toute classe implémente également Equals (type) pour son propre type, afin d'améliorer les performances.
  •   
     

Par défaut, l'opérateur == teste l'égalité des références en déterminant si deux références indiquent le même objet. Par conséquent, les types de référence ne doivent pas nécessairement implémenter l'opérateur == pour bénéficier de cette fonctionnalité. Lorsqu'un type est immuable, c'est-à-dire que les données contenues dans l'instance ne peuvent pas être modifiées, l'opérateur de surcharge == pour comparer l'égalité de valeur au lieu d'égalité de référence peut être utile car, en tant qu'objets immuables, ils peuvent être considérés comme identiques 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és ne doivent pas générer d'exceptions.
  •   
  • Tout type qui surcharge l'opérateur == doit é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 chaîne, == renvoie true si ses deux opérandes font référence au même objet.
  •   
  • Pour le type de chaîne, == compare les valeurs des chaînes.
  •   
  • Lorsque vous testez la valeur null en utilisant des comparaisons == dans votre remplacement par l'opérateur ==, assurez-vous d'utiliser l'opérateur de la classe d'objets de base. Si vous ne le faites pas, une récursion infinie se produira, entraînant 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 permettent d'implémenter un type de valeur :

     
      
  • Envisagez de remplacer Equals pour obtenir des performances supérieures à celles fournies par l'implémentation par défaut d'EqualS on ValueType.
  •   
  • Si vous substituez Equals et que la langue prend en charge la surcharge d'opérateurs, vous devez surcharger l'opérateur d'égalité pour votre type de valeur.
  •   
     

Les instructions suivantes permettent d'implémenter un type de référence :

     
      
  • Envisagez de remplacer Equals par un type de référence si la sémantique de ce 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, telle qu'un type de nombre complexe, vous devez remplacer l'opérateur d'égalité.
  •   

Autres pièges

Cet article recommande simplement de ne pas redéfinir l'opérateur d'égalité (pour les types de référence), et non de redéfinir Equals. Vous devez remplacer Equals dans votre objet (référence ou valeur) si les contrôles d'égalité signifient davantage que les contrôles de référence. Si vous souhaitez une interface, vous pouvez également implémenter IEquatable (utilisé par collections génériques). Toutefois, si vous implémentez IEquatable, vous devez également remplacer les expressions égales, comme indiqué dans la section Remarques d'equatable:

  

Si vous implémentez IEquatable < T > ;, 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 < T > .Equals. Si vous substituez Object.Equals (Object), votre implémentation substitué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 invocations de la méthode Equals renvoient des résultats cohérents.

Pour savoir si vous devez implémenter Equals et / ou l'opérateur d'égalité:

De Implémentation de la méthode Equals

  

La plupart des types de référence ne doivent pas surcharger l'opérateur d'égalité, même s'ils remplacent Equals.

De Instructions pour la mise en oeuvre d'Equal 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 en sorte qu'ils fassent la même chose.

Cela indique seulement que vous devez remplacer Equals chaque fois que vous implémentez l'opérateur d'égalité. ne dit pas que vous devez redéfinir l'opérateur d'égalité lorsque vous redéfinissez Equals.

Pour les objets complexes qui donneront des comparaisons spécifiques, implémenter IComparable et définir la comparaison dans les méthodes de comparaison est une bonne implémentation.

Par exemple, nous avons " Véhicule " les objets dont la seule différence peut être le numéro d’enregistrement et que nous utilisons pour comparer les résultats afin de nous assurer que la valeur attendue renvoyée lors du test est celle que nous souhaitons.

J'ai tendance à utiliser ce que Resharper fabrique automatiquement. par exemple, il crée automatiquement cela 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 souhaitez remplacer == tout en continuant les vérifications des références, vous pouvez toujours utiliser Object.ReferenceEquals .

Microsoft semble avoir modifié sa mélodie ou, du moins, il existe 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é de valeur pour une 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 les opérateurs == et! =, Mais ce n'est pas obligatoire. "

Selon Eric Lippert dans son réponse à une question que j'ai posée à propos de Code minimal pour l'égalité en C # - il dit:

" Le danger que vous rencontrez ici est que vous obteniez 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 == référence l'égalité, puis vous utilisez accidentellement l'égalité de référence sur des éléments non égal à égal à valeur égale. C’est une pratique sujette aux erreurs qui est difficile à repérer lors de l’examen du code humain.

Il y a quelques années, j'ai travaillé sur un algorithme d'analyse statique pour détecter statistiquement cette situation. Nous avons constaté un taux de défauts d'environ deux instances par million de lignes de code dans toutes les bases de code que nous avons étudiées. Si l’on considère uniquement les bases de code qui ont été quelque part écrasées sur Egals, le taux de défauts était bien entendu beaucoup plus élevé!

De plus, tenez compte des coûts et des risques. Si vous avez déjà des implémentations de IComparable, l'écriture de tous les opérateurs est un one-liners trivial qui ne comportera pas de bogue et ne sera jamais modifié. C'est le code le moins cher que vous allez écrire. Si on me donne le choix entre le coût fixe d’écriture et de test d’une douzaine de méthodes minuscules par rapport au coût sans bornes de la recherche et de la correction d’un bogue difficile à voir dans lequel l’égalité de référence est utilisée à la place de l’égalité de valeur, je sais laquelle je choisirais. ;

Le .NET Framework n'utilisera jamais == ou! = avec aucun type que vous n'écriviez. Mais le danger est ce qui arriverait si quelqu'un d'autre le faisait. Donc, si le cours est pour une 3ème partie, je fournirais toujours les opérateurs == et! =. Si la classe est uniquement destinée à être utilisée en interne par le groupe, je mettrais probablement toujours en œuvre les opérateurs == et! =.

Je ne mettrais en œuvre que les opérateurs < ;, < =, > ;, et > = si IComparable était implémenté. IComparable ne devrait être implémenté que si le type doit prendre en charge la commande - comme lors du tri ou utilisé dans un conteneur générique commandé comme 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 alors sage de l’appliquer avec un outil d’analyse de code Q / A qui signale toute occurrence des opérateurs == et! = Lorsqu’il est utilisé avec un type de référence.

Je pense qu'obtenir quelque chose d'aussi simple que la vérification correcte de l'égalité des objets est un peu délicat avec la conception de .NET.

Pour la structure

1) Implémentez IEquatable < T > . Cela améliore sensiblement les performances.

2) Etant donné que vous avez maintenant votre propre est égal à , remplacez GetHashCode et par souci de cohérence avec les différentes substitutions de contrôle d'égalité object.Equals ainsi.

3) La surcharge des opérateurs == et ! = = n'a pas besoin d'être faite de manière religieuse, car le compilateur vous avertira si vous assimilez involontairement une structure à une autre avec un = = ou ! = , mais il est bon de le faire pour être cohérent avec les méthodes est égal à .

public struct Entity : IEquatable<Entity>
{
    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

À partir 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, == ressemble à une égalité de valeur, plus à un sucre syntaxique pour la méthode Equals . Écrire a == b est beaucoup plus intuitif que d'écrire a.Equals (b) . Rarement, nous devrons vérifier l’égalité des références. Dans les niveaux abstraits traitant de représentations logiques d'objets physiques, ce n'est pas quelque chose que nous devrions vérifier. Je pense qu'avoir une sémantique différente pour == et est égal à peut en réalité être déroutant. Je crois que cela aurait dû être == pour l'égalité de valeur et est égal à pour référence (ou un meilleur nom comme IsSameAs ) en premier lieu. J'aimerais ne pas prendre les directives MS au sérieux, pas seulement parce que ce n'est pas naturel pour moi, mais aussi parce que la surcharge == ne fait pas de mal majeur. Cela ne ressemble pas à ne pas écraser Equals ou GetHashCode non génériques, ce qui peut provoquer des dégâts, car le framework n'utilise == nulle part, mais seulement si nous-mêmes utilise le. 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 pas le contrôle de. Et c’est vraiment une grande chose, donc je vais malheureusement y rester .

Avec la sémantique de référence (objets mutables)

1) Remplacer est égal à et GetHashCode .

2) L'implémentation de IEquatable < T > n'est pas obligatoire, mais sera utile si vous en avez un.

public class Entity : IEquatable<Entity>
{
    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 la sémantique de valeur (objets immuables)

C’est la partie la plus délicate. Peut être facilement foiré s'il n'est pas pris en charge.

1) Remplacer est égal à et GetHashCode .

2) Surchargez == et ! = pour correspondre à est égal à . Assurez-vous que cela fonctionne pour les NULL .

2) L'implémentation de IEquatable < T > n'est pas obligatoire, mais sera utile si vous en avez un.

public class Entity : IEquatable<Entity>
{
    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...");
    }
}

Faites particulièrement attention à ce que cela va donner si votre classe peut être héritée. Dans ce cas, vous devrez déterminer si un objet de classe de base peut être égal à un objet de classe dérivé. Idéalement, si aucun objet de la classe dérivée n’est utilisé pour la vérification de l’égalité, une instance de classe de base peut être identique à une instance de classe dérivée et, dans ce cas, il n’est pas nécessaire de vérifier l’égalité Type dans generic < code> est égal à de la classe de base.

En général, veillez à ne pas dupliquer le code. J'aurais pu créer une classe de base abstraite générique ( IEqualizable < T > ou autre) comme gabarit facilitant la réutilisation, mais malheureusement en C # qui m'empêche de dériver de classes supplémentaires.

Toutes les réponses ci-dessus ne prennent pas en compte le polymorphisme. Vous souhaitez souvent que les références dérivées utilisent Equals, même lorsqu'elles sont comparées via une référence de base. Veuillez consulter la question / discussion / réponses ici - Égalité et polymorphisme

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top