web-dev-qa-db-fra.com

Pourquoi devons-nous définir à la fois == et! = En C #?

Le compilateur C # requiert que chaque fois qu'un type personnalisé définit l'opérateur ==, Il doit également définir != (Voir ici ).

Pourquoi?

Je suis curieux de savoir pourquoi les concepteurs l'ont jugé nécessaire et pourquoi le compilateur ne peut-il pas adopter par défaut une implémentation raisonnable pour l'un ou l'autre des opérateurs alors que seul l'autre est présent. Par exemple, Lua vous permet de définir uniquement l'opérateur d'égalité et vous obtenez l'autre gratuitement. C # pourrait faire de même en vous demandant de définir soit == soit les deux == et! =, Puis de compiler automatiquement l'opérateur! = Manquant en tant que !(left == right).

Je comprends qu'il y a des cas étranges où certaines entités ne peuvent être ni égales ni inégales (comme celles de l'IEEE-754 NaN), mais celles-ci semblent être l'exception, pas la règle. Cela n'explique donc pas pourquoi les concepteurs du compilateur C # ont fait de l'exception la règle.

J'ai vu des cas de mauvaise exécution où l'opérateur d'égalité est défini, puis l'opérateur d'inégalité est un copier-coller avec chaque comparaison inversée et chaque && basculé vers un || (vous obtenez le point ... en gros! (a == b) développé par les règles de De Morgan). C'est une mauvaise pratique que le compilateur pourrait éliminer par conception, comme c'est le cas avec Lua.

Remarque: Il en va de même pour les opérateurs <> <=> =. Je ne peux pas imaginer des cas où vous aurez besoin de les définir de manière non naturelle. Lua vous permet de définir uniquement <et <= et définit> = et> naturellement à travers la négation des formateurs. Pourquoi C # ne fait-il pas de même (au moins "par défaut")?

[~ # ~] modifier [~ # ~]

Apparemment, il existe des raisons valables pour permettre au programmeur de mettre en œuvre des contrôles d'égalité et d'inégalité comme bon lui semble. Certaines réponses indiquent des cas où cela peut être agréable.

Le noyau de ma question, cependant, est pourquoi cela est requis de force en C # quand généralement ce n'est pas logiquement nécessaire?

C'est également un contraste frappant avec les choix de conception pour les interfaces .NET comme Object.Equals, IEquatable.EqualsIEqualityComparer.Equals Où l'absence d'un homologue NotEquals montre que le cadre considère !Equals() objets comme inégaux et c'est tout. De plus, des classes comme Dictionary et des méthodes comme .Contains() dépendent exclusivement des interfaces susmentionnées et n'utilisent pas directement les opérateurs même s'ils sont définis. En fait, lorsque ReSharper génère des membres d'égalité, il définit à la fois == Et != En termes de Equals() et même alors seulement si l'utilisateur choisit de générer des opérateurs. Les opérateurs d'égalité ne sont pas nécessaires au framework pour comprendre l'égalité des objets.

Fondamentalement, le framework .NET ne se soucie pas de ces opérateurs, il ne se soucie que de quelques méthodes Equals. La décision d'exiger que les opérateurs == et! = Soient définis en tandem par l'utilisateur est uniquement liée à la conception du langage et non à la sémantique des objets en ce qui concerne .NET.

340
Stefan Dragnev

Je ne peux pas parler pour les concepteurs de langage, mais d'après ce que je peux raisonner, il semble que c'était une décision de conception intentionnelle et appropriée.

En regardant ce code F # de base, vous pouvez le compiler dans une bibliothèque de travail. C'est un code légal pour F #, et ne surcharge que l'opérateur d'égalité, pas l'inégalité:

module Module1

type Foo() =
    let mutable myInternalValue = 0
    member this.Prop
        with get () = myInternalValue
        and set (value) = myInternalValue <- value

    static member op_Equality (left : Foo, right : Foo) = left.Prop = right.Prop
    //static member op_Inequality (left : Foo, right : Foo) = left.Prop <> right.Prop

Cela fait exactement à quoi il ressemble. Il crée un comparateur d'égalité sur == uniquement, et vérifie si les valeurs internes de la classe sont égales.

Bien que vous ne puissiez pas créer une classe comme celle-ci en C #, vous pouvez en utiliser une qui a été compilée pour .NET. Il est évident qu'il utilisera notre opérateur surchargé pour == Alors, qu'est-ce que le runtime utilise pour !=?

La norme C # EMCA contient tout un tas de règles (section 14.9) expliquant comment déterminer quel opérateur utiliser lors de l'évaluation de l'égalité. Pour le dire trop simplifié et donc pas parfaitement précis, si les types qui sont comparés sont du même type et il y a un opérateur d'égalité surchargé présent, il utilisera cette surcharge et non le opérateur d'égalité de référence standard hérité d'Object. Il n'est donc pas surprenant que si un seul des opérateurs est présent, il utilisera l'opérateur d'égalité de référence par défaut, que tous les objets ont, il n'y a pas de surcharge pour lui.1

Sachant que c'est le cas, la vraie question est: pourquoi a-t-il été conçu de cette manière et pourquoi le compilateur ne le comprend-il pas tout seul? Beaucoup de gens disent que ce n'était pas une décision de conception, mais j'aime à penser que cela a été pensé de cette façon, en particulier en ce qui concerne le fait que tous les objets ont un opérateur d'égalité par défaut.

Alors, pourquoi le compilateur ne crée-t-il pas automatiquement le != opérateur? Je ne peux pas être sûr à moins que quelqu'un de Microsoft ne le confirme, mais c'est ce que je peux déterminer en raisonnant sur les faits.


Pour éviter un comportement inattendu

Peut-être que je veux faire une comparaison de valeurs sur == pour tester l'égalité. Cependant, en ce qui concerne != Je ne me souciais pas du tout que les valeurs soient égales à moins que la référence ne soit égale, car pour que mon programme les considère égales, je ne me soucie que si les références correspondent. Après tout, cela est en fait décrit comme un comportement par défaut du C # (si les deux opérateurs n'étaient pas surchargés, comme ce serait le cas pour certaines bibliothèques .net écrites dans une autre langue). Si le compilateur ajoutait du code automatiquement, je ne pouvais plus compter sur le compilateur pour produire du code qui devrait être conforme. Le compilateur ne doit pas écrire de code caché qui modifie le comportement du vôtre, en particulier lorsque le code que vous avez écrit respecte les normes de C # et de CLI.

En termes de forçage vous le surchargez, au lieu d'aller au comportement par défaut, je peux seulement dire fermement qu'il est dans la norme (EMCA-334 17.9.2)2. La norme ne précise pas pourquoi. Je crois que cela est dû au fait que C # emprunte beaucoup de comportement à C++. Voir ci-dessous pour en savoir plus.


Lorsque vous remplacez != et ==, vous n'avez pas à renvoyer bool.

C'est une autre raison probable. En C #, cette fonction:

public static int operator ==(MyClass a, MyClass b) { return 0; }

est aussi valide que celui-ci:

public static bool operator ==(MyClass a, MyClass b) { return true; }

Si vous retournez autre chose que bool, le compilateur ne peut pas déduit automatiquement un type opposé. De plus, dans le cas où votre opérateur le fait retourne booléen, cela n'a tout simplement pas de sens pour eux de créer du code qui n'existerait que dans ce cas spécifique ou, comme je l'ai dit plus haut, du code qui masque le comportement par défaut du CLR.


C # emprunte beaucoup au C++3

Lorsque C # a été introduit, il y avait un article dans le magazine MSDN qui écrivait, parlant de C #:

De nombreux développeurs souhaitent qu'il y ait un langage facile à écrire, à lire et à entretenir comme Visual Basic, mais qui offre toujours la puissance et la flexibilité de C++.

Oui, l'objectif de conception de C # était de donner presque la même quantité de puissance que C++, en ne sacrifiant qu'un peu pour des commodités telles que la sécurité de type rigide et la collecte des ordures. C # a été fortement modélisé après C++.

Vous ne serez peut-être pas surpris d'apprendre qu'en C++, les opérateurs d'égalité n'ont pas à renvoyer bool , comme indiqué dans cet exemple de programme

Maintenant, C++ ne vous oblige pas directement à exiger à surcharger l'opérateur complémentaire. Si vous avez compilé le code dans l'exemple de programme, vous verrez qu'il s'exécute sans erreur. Cependant, si vous avez essayé d'ajouter la ligne:

cout << (a != b);

tu auras

erreur du compilateur C2678 (MSVC): binaire '! =': aucun opérateur trouvé qui prend un opérande gauche de type 'Test' (ou il n'y a pas de conversion acceptable) '.

Ainsi, alors que C++ lui-même ne vous oblige pas à surcharger par paires, il ne vous permettra pas d'utiliser un opérateur d'égalité que vous n'avez pas surchargé sur un classe personnalisée. Il est valide dans .NET, car tous les objets en ont un par défaut; C++ ne fonctionne pas.


1. En remarque, la norme C # vous oblige toujours à surcharger la paire d'opérateurs si vous souhaitez surcharger l'un ou l'autre. Cela fait partie du standard et pas simplement du compilateur. Cependant, les mêmes règles concernant la détermination de l'opérateur à appeler s'appliquent lorsque vous accédez à une bibliothèque .net écrite dans une autre langue qui n'a pas les mêmes exigences.

2. EMCA-334 (pdf) ( http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf )

3. Et Java, mais ce n'est vraiment pas le point ici

157

Probablement si quelqu'un a besoin d'implémenter une logique à trois valeurs (c'est-à-dire null). Dans des cas comme celui-ci - SQL standard ANSI, par exemple - les opérateurs ne peuvent pas simplement être annulés en fonction de l'entrée.

Vous pourriez avoir un cas où:

var a = SomeObject();

Et a == true renvoie false et a == false renvoie également false.

48
Yuck

À part cela, C # diffère en C++ dans de nombreux domaines, la meilleure explication à laquelle je peux penser est que dans certains cas, vous voudrez peut-être adopter une légèrement approche différente à prouver "non l'égalité" qu'à prouver "l'égalité".

Évidemment, avec la comparaison de chaînes, par exemple, vous pouvez simplement tester l'égalité et return hors de la boucle lorsque vous voyez des caractères qui ne correspondent pas. Cependant, il pourrait ne pas être aussi propre avec des problèmes plus compliqués. Le filtre de floraison vient à l'esprit; il est très facile de dire rapidement si l'élément est pas dans l'ensemble, mais difficile de dire si l'élément est dans l'ensemble. Bien que la même technique return puisse s'appliquer, le code n'est peut-être pas aussi joli.

24
Brian Gordon

Si vous regardez les implémentations de surcharges de == et! = Dans la source .net, elles n'implémentent souvent pas! = Comme! (Gauche == droite). Ils l'implémentent complètement (comme ==) avec une logique négative. Par exemple, DateTime implémente == comme

return d1.InternalTicks == d2.InternalTicks;

et! = as

return d1.InternalTicks != d2.InternalTicks;

Si vous (ou le compilateur s'il le faisait implicitement) deviez implémenter! = As

return !(d1==d2);

alors vous faites une hypothèse sur l'implémentation interne de == et! = dans les choses que votre classe référence. Éviter cette hypothèse peut être la philosophie derrière leur décision.

20
hatchet

Pour répondre à votre modification, concernant la raison pour laquelle vous êtes obligé de remplacer les deux si vous en remplacez un, tout est dans l'héritage.

Si vous remplacez ==, le plus susceptible de fournir une sorte d'égalité sémantique ou structurelle (par exemple, DateTimes sont égaux si leurs propriétés InternalTicks sont égales même si elles peuvent être différentes instances), alors vous changez le comportement par défaut de l'opérateur de Object, qui est le parent de tous les objets .NET. L'opérateur == est, en C #, une méthode dont l'implémentation de base Object.operator (==) effectue une comparaison référentielle. Object.operator (! =) Est une autre méthode différente, qui effectue également une comparaison référentielle.

Dans presque tous les autres cas de substitution de méthode, il serait illogique de présumer que la substitution d'une méthode entraînerait également un changement de comportement vers une méthode antonymique. Si vous avez créé une classe avec les méthodes Increment () et Decrement () et que vous avez remplacé Increment () dans une classe enfant, vous attendriez-vous à ce que Decrement () soit également remplacé par l'opposé de votre comportement substitué? Le compilateur ne peut pas être rendu suffisamment intelligent pour générer une fonction inverse pour toute implémentation d'un opérateur dans tous les cas possibles.

Cependant, les opérateurs, bien qu'implémentés de manière très similaire aux méthodes, fonctionnent conceptuellement par paires; == et! =, <et>, et <= et> =. Il serait illogique dans ce cas du point de vue d'un consommateur de penser que! = Fonctionne différemment de ==. Donc, le compilateur ne peut pas supposer que a! = B ==! (A == b) dans tous les cas, mais on s'attend généralement à ce que == et! = Fonctionnent de la même manière, donc le compilateur force vous mettre en œuvre par paires, mais vous finissez par le faire. Si, pour votre classe, a! = B ==! (A == b), alors implémentez simplement l'opérateur! = En utilisant! (==), mais si cette règle ne s'applique pas dans tous les cas à votre objet (par exemple , si la comparaison avec une valeur particulière, égale ou inégale, n'est pas valide), alors vous devez être plus intelligent que l'IDE.

La VRAIE question à poser est pourquoi <et> et <= et> = sont des paires d'opérateurs comparatifs qui doivent être implémentées simultanément, quand en termes numériques! (A <b) == a> = b et! (A> b) == a <= b. Vous devriez être tenu de mettre en œuvre les quatre si vous en remplacez un, et vous devriez probablement aussi être obligé de remplacer == (et! =), Car (a <= b) == (a == b) si a est sémantiquement égal à b.

16
KeithS

Si vous surchargez == pour votre type personnalisé, et non! = Alors il sera géré par l'opérateur! = Pour l'objet! = Objet car tout est dérivé de l'objet, et ce serait très différent de CustomType! = CustomType.

De plus, les créateurs de langage l'ont probablement voulu de cette façon pour permettre le plus de flexibilité possible aux codeurs, et aussi pour qu'ils ne fassent pas d'hypothèses sur ce que vous avez l'intention de faire.

12
Chris Mullins

C'est ce qui me vient à l'esprit en premier:

  • Et si tester l'inégalité est beaucoup plus rapide que tester l'égalité?
  • Et si dans certains cas vous voulez renvoyer false les deux pour == et != (c'est-à-dire s'ils ne peuvent pas être comparés pour une raison quelconque)
9
Dan Abramov

Les mots clés de votre question sont "pourquoi" et "must".

Par conséquent:

Répondre de cette façon parce qu'ils l'ont conçu ainsi, c'est vrai ... mais ne pas répondre à la partie "pourquoi" de votre question.

Répondre qu'il peut parfois être utile de remplacer ces deux éléments indépendamment, est vrai ... mais ne pas répondre à la partie "incontournable" de votre question.

Je pense que la réponse simple est qu'il n'y a aucune raison convaincante pour laquelle C # nécessite vous de remplacer les deux.

La langue doit vous permettre de remplacer uniquement ==, et vous fournit une implémentation par défaut de != C'est ! cette. Si vous souhaitez remplacer != aussi.

Ce n'était pas une bonne décision. Les humains conçoivent des langages, les humains ne sont pas parfaits, C # n'est pas parfait. Haussement d'épaules et Q.E.D.

5
Greg Hendershott

Eh bien, c'est probablement juste un choix de conception, mais comme vous le dites, x!= y Ne doit pas être identique à !(x == y). En n'ajoutant pas d'implémentation par défaut, vous êtes certain de ne pas oublier d'implémenter une implémentation spécifique. Et si c'est en effet aussi trivial que vous le dites, vous pouvez simplement implémenter l'un en utilisant l'autre. Je ne vois pas en quoi c'est une "mauvaise pratique".

Il peut également y avoir d'autres différences entre C # et Lua ...

4
GolezTrol

Juste pour ajouter aux excellentes réponses ici:

Considérez ce qui se passerait dans le débogueur, lorsque vous essayez d'entrer dans un != opérateur et se retrouver dans un == opérateur à la place! Parlez de confusion!

Il est logique que CLR vous permette de laisser de côté l'un ou l'autre des opérateurs - car il doit fonctionner avec de nombreuses langues. Mais il existe de nombreux exemples de C # qui n'exposent pas les fonctionnalités CLR (ref retours et locals, par exemple), et de nombreux exemples d'implémentation de fonctionnalités qui ne sont pas dans le CLR lui-même (par exemple: using, lock, foreach, etc.).

3
Andrew Russell

Bref, une cohérence forcée.

'==' et '! =' sont toujours de vrais opposés, quelle que soit la façon dont vous les définissez, définis comme tels par leur définition verbale de "égal" et "non égal". En ne définissant qu'un seul d'entre eux, vous vous ouvrez à une incohérence d'opérateur d'égalité où '==' et '! =' Peuvent tous les deux être vrais ou tous les deux faux pour deux valeurs données. Vous devez définir les deux, car lorsque vous choisissez d'en définir un, vous devez également définir l'autre de manière appropriée afin qu'il soit clairement clair quelle est votre définition de "l'égalité". L'autre solution pour le compilateur est de vous autoriser uniquement à remplacer '==' OR '! =' Et à laisser l'autre comme négativement intrinsèque à l'autre. Évidemment, ce n'est pas le cas avec le compilateur C # et je suis sûr qu'il y a une raison valable à cela qui peut être attribuée strictement comme un choix de simplicité.

La question que vous devriez vous poser est "pourquoi dois-je remplacer les opérateurs?" C'est une décision forte à prendre qui nécessite un raisonnement fort. Pour les objets, '==' et '! =' Comparent par référence. Si vous devez les remplacer pour ne PAS comparer par référence, vous créez une incohérence d'opérateur générale qui n'est apparente à aucun autre développeur qui lirait ce code. Si vous essayez de poser la question "l'état de ces deux instances est-il équivalent?", Vous devez alors implémenter IEquatible, définir Equals () et utiliser cet appel de méthode.

Enfin, IEquatable () ne définit pas NotEquals () pour le même raisonnement: possibilité d'ouvrir des incohérences d'opérateur d'égalité. NotEquals () devrait TOUJOURS retourner! Equals (). En ouvrant la définition de NotEquals () à la classe implémentant Equals (), vous forcez à nouveau la question de la cohérence dans la détermination de l'égalité.

Edit: C'est tout simplement mon raisonnement.

2
Emoire

Les langages de programmation sont des réarrangements syntaxiques d'instructions logiques exceptionnellement complexes. Dans cet esprit, pouvez-vous définir un cas d'égalité sans définir un cas de non-égalité? La réponse est non. Pour qu'un objet a soit égal à l'objet b, alors l'inverse de l'objet a différent de b doit également être vrai. Une autre façon de le montrer est

if a == b then !(a != b)

cela permet au langage de déterminer l’égalité des objets. Par exemple, la comparaison NULL! = NULL peut jeter une clé dans la définition d'un système d'égalité qui n'implémente pas de déclaration de non-égalité.

Maintenant, en ce qui concerne l'idée de! = Étant simplement une définition remplaçable comme dans

if !(a==b) then a!=b

Je ne peux pas contester cela. Cependant, c'était probablement une décision du groupe de spécification du langage C # que le programmeur soit obligé de définir explicitement l'égalité et la non-égalité d'un objet ensemble

2
Josh