wake-up-neo.com

Warum müssen wir in C # == und! = Definieren?

Der C # -Compiler verlangt, dass ein benutzerdefinierter Typ immer dann, wenn er den Operator == Definiert, auch != Definiert (siehe hier ).

Warum?

Ich bin gespannt, warum die Designer dies für notwendig hielten und warum der Compiler für keinen der Operatoren eine sinnvolle Implementierung vorschreiben kann, wenn nur der andere vorhanden ist. Mit Lua können Sie zum Beispiel nur den Gleichheitsoperator definieren, und den anderen erhalten Sie kostenlos. C # könnte dasselbe tun, indem Sie entweder == oder beide == und! = Definieren und dann den fehlenden! = -Operator automatisch als !(left == right) kompilieren.

Ich verstehe, dass es seltsame Eckfälle gibt, in denen einige Entitäten möglicherweise weder gleich noch ungleich sind (wie IEEE-754-NaNs), aber diese scheinen die Ausnahme zu sein, nicht die Regel. Das erklärt also nicht, warum die C # -Compiler-Designer die Ausnahme zur Regel gemacht haben.

Ich habe Fälle von schlechter Verarbeitung gesehen, in denen der Gleichheitsoperator definiert ist, dann ist der Ungleichheitsoperator ein Copy-Paste, bei dem jeder Vergleich umgekehrt und jeder && auf ein || umgeschaltet wird (Sie verstehen ... im Grunde genommen! (a == b) erweitert durch De Morgans Regeln). Das ist eine schlechte Praxis, die der Compiler wie bei Lua durch das Design eliminieren könnte.

Hinweis: Gleiches gilt für die Operatoren <> <=> =. Ich kann mir keine Fälle vorstellen, in denen Sie diese auf unnatürliche Weise definieren müssen. Lua lässt Sie nur <und <= definieren und definiert> = und> auf natürliche Weise durch die Negation des Formers. Warum macht C # nicht dasselbe (zumindest "standardmäßig")?

[~ # ~] edit [~ # ~]

Anscheinend gibt es triftige Gründe, dem Programmierer zu erlauben, Prüfungen auf Gleichheit und Ungleichheit durchzuführen, wie sie wollen. Einige der Antworten verweisen auf Fälle, in denen das Nizza sein könnte.

Der Kern meiner Frage ist jedoch, warum dies in C # zwangsweise erforderlich ist, wenn normalerweise es nicht logisch notwendig ist?

Dies steht auch in auffälligem Kontrast zu den Entwurfsoptionen für .NET-Schnittstellen wie Object.Equals, IEquatable.EqualsIEqualityComparer.Equals, Bei denen das Fehlen eines Gegenstücks zu NotEquals zeigt, dass das Framework dies berücksichtigt !Equals() Objekte als ungleich und das ist das. Darüber hinaus hängen Klassen wie Dictionary und Methoden wie .Contains() ausschließlich von den oben genannten Schnittstellen ab und verwenden die Operatoren nicht direkt, auch wenn sie definiert sind. Tatsächlich definiert ReSharper beim Generieren von Gleichheitsmitgliedern sowohl == Als auch != In Bezug auf Equals() und dies auch dann, wenn der Benutzer überhaupt Operatoren generieren möchte. Die Gleichheitsoperatoren werden vom Framework nicht benötigt, um die Objektgleichheit zu verstehen.

Grundsätzlich kümmert sich das .NET-Framework nicht um diese Operatoren, sondern nur um einige Equals -Methoden. Die Entscheidung, die beiden Operatoren == und! = Gemeinsam vom Benutzer zu definieren, hängt rein vom Sprachdesign und nicht von der Objektsemantik in Bezug auf .NET ab.

340
Stefan Dragnev

Ich kann nicht für die Sprachdesigner sprechen, aber soweit ich das beurteilen kann, scheint es eine beabsichtigte, richtige Designentscheidung gewesen zu sein.

Wenn Sie sich diesen grundlegenden F # -Code ansehen, können Sie ihn in eine funktionierende Bibliothek kompilieren. Dies ist der zulässige Code für F # und überlastet nur den Gleichheitsoperator, nicht die Ungleichung:

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

Das macht genau das, wie es aussieht. Es erstellt einen Gleichheitsvergleich nur für == Und prüft, ob die internen Werte der Klasse gleich sind.

Solch eine Klasse können Sie in C # nicht erstellen, aber Sie können can eine verwenden, die für .NET kompiliert wurde. Es ist offensichtlich, dass unser überladener Operator für == Verwendet wird. Was verwendet die Laufzeit für !=?

Der C # EMCA-Standard enthält eine ganze Reihe von Regeln (Abschnitt 14.9), die erläutern, wie zu bestimmen ist, welcher Operator bei der Bewertung der Gleichheit zu verwenden ist. Um es zu vereinfachen und damit nicht ganz genau zu sagen: Wenn die zu vergleichenden Typen vom selben Typ sind und, ist ein überladener Gleichheitsoperator vorhanden, der diese Überladung verwendet und nicht die Standardreferenz-Gleichheitsoperator, der von Object geerbt wurde. Es ist daher nicht verwunderlich, dass, wenn nur einer der Operatoren vorhanden ist, der Standard-Referenzgleichheitsoperator verwendet wird, den alle Objekte haben und für den es keine Überladung gibt.1

In dem Wissen, dass dies der Fall ist, lautet die eigentliche Frage: Warum wurde dies auf diese Weise entworfen und warum kann der Compiler es nicht selbst herausfinden? Viele Leute sagen, dass dies keine Designentscheidung war, aber ich denke gerne, dass dies so gedacht wurde, insbesondere im Hinblick darauf, dass alle Objekte einen Standard-Gleichheitsoperator haben.

Warum erstellt der Compiler den Operator != Nicht automatisch? Ich kann es nicht genau wissen, es sei denn, jemand von Microsoft bestätigt dies. Dies kann ich jedoch anhand der Argumentation zu den Fakten feststellen.


Um unerwartetes Verhalten zu verhindern

Vielleicht möchte ich einen Wertevergleich für == Durchführen, um die Gleichheit zu testen. Wenn es jedoch um != Ging, war es mir egal, ob die Werte gleich waren, es sei denn, die Referenz war gleich, da es für mein Programm nur wichtig ist, dass die Referenzen übereinstimmen. Immerhin wird dies als Standardverhalten von C # beschrieben (wenn beide Operatoren nicht überladen wären, wie es bei einigen .net-Bibliotheken der Fall wäre, die in einer anderen Sprache geschrieben wurden). Wenn der Compiler automatisch Code hinzufügt, kann ich mich nicht mehr darauf verlassen, dass der Compiler den Code ausgibt, der kompatibel sein sollte. Der Compiler sollte keinen verborgenen Code schreiben, der das Verhalten Ihres Codes ändert, insbesondere wenn der von Ihnen geschriebene Code innerhalb der Standards von C # und der CLI liegt.

In Bezug auf es Forcen Sie es zu überlasten, anstatt auf das Standardverhalten zu gehen, kann ich nur fest sagen, dass es im Standard ist (EMCA-334 17.9.2)2. Die Norm legt nicht fest, warum. Ich glaube, das liegt an der Tatsache, dass C # viel Verhalten von C++ entlehnt. Mehr dazu weiter unten.


Wenn Sie != Und == Überschreiben, müssen Sie bool nicht zurückgeben.

Dies ist ein weiterer wahrscheinlicher Grund. In C # ist diese Funktion:

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

ist so gültig wie dieser:

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

Wenn Sie etwas anderes als bool zurückgeben, leitet der Compiler kann nicht automatisch einen anderen Typ ab. Außerdem ist es in dem Fall, in dem Ihr Operator does bool zurückgibt, nicht sinnvoll, Code zu generieren, der nur in diesem einen speziellen Fall oder, wie oben erwähnt, Code existieren würde das verbirgt das Standardverhalten der CLR.


C # leiht sich viel von C++ aus3

Als C # eingeführt wurde, gab es einen Artikel im MSDN-Magazin, der über C # schrieb:

Viele Entwickler wünschten, es gäbe eine Sprache, die einfach zu schreiben, zu lesen und zu warten ist wie Visual Basic, aber dennoch die Leistungsfähigkeit und Flexibilität von C++ bietet.

Ja, das Entwurfsziel für C # war es, nahezu die gleiche Leistung wie für C++ bereitzustellen, wobei nur ein geringer Verlust für Annehmlichkeiten wie starre Typensicherheit und Garbage Collection zu verzeichnen war. C # wurde stark nach C++ modelliert.

Es mag Sie nicht überraschen, dass die Gleichheitsoperatoren in C++ nicht bool zurückgeben müssen, wie in dieses Beispielprogramms gezeigt

Nun, C++ benötigt nicht direkt Sie, um den komplementären Operator zu überladen. Wenn Sie den Code im Beispielprogramm kompiliert haben, sehen Sie, dass er fehlerfrei ausgeführt wird. Wenn Sie jedoch versucht haben, die Zeile hinzuzufügen:

cout << (a != b);

sie erhalten

compilerfehler C2678 (MSVC): binär '! =': Es wurde kein Operator gefunden, der einen linken Operanden vom Typ 'Test' annimmt (oder es gibt keine akzeptable Konvertierung).

Während Sie in C++ nicht paarweise überladen müssen, können Sie mit will not ​​einen Gleichheitsoperator verwenden, den Sie für eine benutzerdefinierte Klasse nicht überladen haben. Es ist in .NET gültig, da alle Objekte ein Standardobjekt haben. C++ nicht.


1. Als Randnotiz verlangt der C # -Standard immer noch, dass Sie das Operatorpaar überladen, wenn Sie eines von beiden überladen möchten. Dies ist ein Teil des Standards und nicht einfach des Compilers. Für den Zugriff auf eine .net-Bibliothek, die in einer anderen Sprache geschrieben ist und nicht die gleichen Anforderungen erfüllt, gelten jedoch dieselben Regeln für die Bestimmung des aufzurufenden Operators.

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

3. Und Java, aber darum geht es hier wirklich nicht

157

Wahrscheinlich für den Fall, dass jemand dreiwertige Logik implementieren muss (d. H. null). In solchen Fällen - zum Beispiel ANSI-Standard-SQL - können die Operatoren nicht einfach je nach Eingabe negiert werden.

Sie könnten einen Fall haben, in dem:

var a = SomeObject();

Und a == true Gibt false zurück und a == false Gibt auch false zurück.

48
Yuck

Abgesehen davon, dass sich C # in vielen Bereichen von C++ unterscheidet, ist die beste Erklärung, die ich mir vorstellen kann, dass Sie in einigen Fällen einen etwas anderen Ansatz wählen möchten zu beweisen "nicht gleich" als zu beweisen "gleich".

Offensichtlich können Sie beispielsweise beim Zeichenfolgenvergleich die Gleichheit und return außerhalb der Schleife testen, wenn nicht übereinstimmende Zeichen angezeigt werden. Es ist jedoch möglicherweise nicht so sauber mit komplizierteren Problemen. Das Blütenfilter fällt mir ein; Es ist sehr einfach, schnell zu erkennen, ob das Element nicht in der Menge enthalten ist , aber es ist schwierig zu erkennen, ob das Element vorhanden ist im Set. Während die gleiche return Technik angewendet werden könnte, ist der Code möglicherweise nicht so hübsch.

24
Brian Gordon

Wenn Sie sich Implementierungen von Überladungen von == und! = In der .net-Quelle ansehen, implementieren sie! = As! (Left == right) häufig nicht. Sie implementieren es vollständig (wie ==) mit negierter Logik. Zum Beispiel implementiert DateTime == als

return d1.InternalTicks == d2.InternalTicks;

und! = als

return d1.InternalTicks != d2.InternalTicks;

Wenn Sie (oder der Compiler, wenn er es implizit tat)! = As implementieren würden

return !(d1==d2);

dann nehmen Sie an, dass == und! = in den Dingen, auf die Ihre Klasse verweist, intern implementiert sind. Diese Annahme zu vermeiden, könnte die Philosophie hinter ihrer Entscheidung sein.

20
hatchet

Um Ihre Bearbeitung zu beantworten und zu prüfen, warum Sie gezwungen sind, beide zu überschreiben, wenn Sie einen überschreiben, liegt alles in der Vererbung.

Wenn Sie == überschreiben, was höchstwahrscheinlich zu einer Art semantischer oder struktureller Gleichheit führt (z. B. sind DateTimes gleich, wenn ihre InternalTicks-Eigenschaften gleich sind, auch wenn es sich um unterschiedliche Instanzen handelt), ändern Sie das Standardverhalten des Operators von Object ist das übergeordnete Objekt aller .NET-Objekte. Der Operator == ist in C # eine Methode, deren Basisimplementierung Object.operator (==) einen referenziellen Vergleich durchführt. Object.operator (! =) Ist eine andere Methode, die auch einen referenziellen Vergleich durchführt.

In fast jedem anderen Fall des Überschreibens von Methoden wäre es unlogisch anzunehmen, dass das Überschreiben einer Methode auch zu einer Verhaltensänderung zu einer antonymischen Methode führen würde. Wenn Sie eine Klasse mit den Methoden Increment () und Decrement () erstellt und Increment () in einer untergeordneten Klasse überschrieben hätten, würden Sie erwarten, dass Decrement () auch mit dem Gegenteil Ihres überschriebenen Verhaltens überschrieben wird? Der Compiler kann nicht schlau genug gemacht werden, um für jede Implementierung eines Operators in allen möglichen Fällen eine Umkehrfunktion zu generieren.

Obwohl Operatoren sehr ähnlich zu Methoden implementiert sind, arbeiten sie konzeptionell paarweise. == und! =, <und> und <= und> =. In diesem Fall wäre es aus Sicht des Verbrauchers unlogisch zu denken, dass! = Anders funktioniert als ==. Der Compiler kann also nicht in allen Fällen davon ausgehen, dass a! = B ==! (A == b) ist, aber es wird allgemein erwartet, dass == und! = Auf ähnliche Weise funktionieren sollten, sodass der Compiler dies erzwingt Sie zu zweit zu implementieren, aber am Ende tun Sie das tatsächlich. Wenn für Ihre Klasse a! = B ==! (A == b) ist, implementieren Sie einfach den Operator! = Mit! (==), aber wenn diese Regel nicht in allen Fällen für Ihr Objekt gilt (zum Beispiel) Wenn der Vergleich mit einem bestimmten Wert (gleich oder ungleich) ungültig ist, müssen Sie schlauer als die IDE sein.

Die WIRKLICHE Frage, die gestellt werden sollte, ist, warum <und> und <= und> = Paare für Vergleichsoperatoren sind, die gleichzeitig implementiert werden müssen, wenn in numerischen Begriffen! (A <b) == a> = b und! (A> b) == a <= b. Sie sollten alle vier implementieren müssen, wenn Sie eine überschreiben, und Sie sollten wahrscheinlich auch == (und! =) Überschreiben müssen, da (a <= b) == (a == b), wenn a semantisch ist gleich b.

16
KeithS

Wenn Sie == für Ihren benutzerdefinierten Typ überladen und nicht! =, Wird dies vom Operator! = Für object! = Object behandelt, da alles von object abgeleitet ist und dies sich erheblich von CustomType! = CustomType unterscheidet.

Wahrscheinlich wollten die Sprachentwickler, dass Codierer auf diese Weise die größtmögliche Flexibilität erhalten und dass sie keine Annahmen darüber treffen, was Sie vorhaben.

12
Chris Mullins

Das fällt mir zuerst ein:

  • Was ist, wenn Testen von Ungleichheit ist viel schneller als Testen von Gleichheit?
  • Was ist, wenn in einigen Fällen Sie möchten false sowohl für == und != (d. h. wenn sie aus irgendeinem Grund nicht verglichen werden können)
9
Dan Abramov

Die Schlüsselwörter in Ihrer Frage sind "warum" und "muss".

Als Ergebnis:

Es ist wahr, dass es so ist, weil sie es so entworfen haben, aber nicht das "Warum" Ihrer Frage zu beantworten.

Die Antwort, dass es manchmal hilfreich sein kann, beide unabhängig voneinander zu überschreiben, ist richtig ... aber nicht das "Muss" Ihrer Frage zu beantworten.

Ich denke, die einfache Antwort ist, dass es ist nicht einen überzeugenden Grund gibt, warum C # erfordert Sie beide überschreiben.

Die Sprache sollte es Ihnen ermöglichen, nur == Zu überschreiben und Ihnen eine Standardimplementierung von != Bereitzustellen, dh !. Wenn Sie zufällig auch != Überschreiben möchten, müssen Sie es tun.

Es war keine gute Entscheidung. Menschen entwerfen Sprachen, Menschen sind nicht perfekt, C # ist nicht perfekt. Shrug und Q.E.D.

5

Nun, es ist wahrscheinlich nur eine Designauswahl, aber wie Sie sagen, muss x!= y Nicht mit !(x == y) identisch sein. Wenn Sie keine Standardimplementierung hinzufügen, können Sie sicher sein, dass Sie nicht vergessen, eine bestimmte Implementierung zu implementieren. Und wenn es tatsächlich so trivial ist, wie Sie sagen, können Sie einfach eins mit dem anderen implementieren. Ich verstehe nicht, wie das "schlechte Praxis" ist.

Es kann auch andere Unterschiede zwischen C # und Lua geben ...

4
GolezTrol

Nur um die hervorragenden Antworten hier zu ergänzen:

Überlegen Sie, was im Debugger passieren würde, wenn Sie versuchen, in ein != Operator und landen in einem == Operator stattdessen! Sprechen Sie über verwirrend!

Es ist sinnvoll, dass CLR Ihnen die Freiheit gibt, den einen oder anderen Operator wegzulassen, da es mit vielen Sprachen arbeiten muss. Es gibt jedoch viele Beispiele dafür, dass C # CLR-Features nicht verfügbar macht (z. B. ref Returns und Locals), und viele Beispiele für die Implementierung von Features, die nicht in der CLR selbst enthalten sind (z. B .: using, lock, foreach, etc).

3
Andrew Russell

Kurz, erzwungene Konsistenz.

'==' und '! =' sind immer wahre Gegensätze, egal wie Sie sie definieren, definiert als solche durch ihre verbale Definition von "gleich" und "nicht gleich". Indem Sie nur einen von ihnen definieren, öffnen Sie sich einer Inkonsistenz von Gleichheitsoperatoren, bei der sowohl '==' als auch '! =' Für zwei gegebene Werte wahr oder beide falsch sein können. Sie müssen beides definieren, da Sie, wenn Sie sich für die Definition des einen entscheiden, auch den anderen angemessen definieren müssen, damit klar ist, was Ihre Definition von "Gleichheit" ist. Die andere Lösung für den Compiler besteht darin, nur das Überschreiben von '==' OR '! =' Zuzulassen und den anderen als inhärent negierend zu betrachten. Offensichtlich ist dies bei nicht der Fall der C # -Compiler und ich bin sicher, es gibt einen triftigen Grund dafür, der möglicherweise nur der Einfachheit halber zuzuschreiben ist.

Die Frage, die Sie stellen sollten, lautet: "Warum muss ich die Operatoren überschreiben?" Das ist eine starke Entscheidung, die starkes Denken erfordert. Bei Objekten vergleichen Sie '==' und '! =' Anhand der Referenz. Wenn Sie sie überschreiben, um einen Vergleich NICHT anhand eines Verweises durchzuführen, erstellen Sie eine allgemeine Operatorinkonsistenz, die für keinen anderen Entwickler erkennbar ist, der diesen Code lesen würde. Wenn Sie versuchen, die Frage "Ist der Status dieser beiden Instanzen äquivalent?" Zu stellen, sollten Sie IEquatible implementieren, Equals () definieren und diesen Methodenaufruf verwenden.

Schließlich definiert IEquatable () NotEquals () nicht aus demselben Grund: Das Potenzial, Inkonsistenzen bei Gleichheitsoperatoren zu verursachen. NotEquals () sollte IMMER zurückkehren! Equals (). Indem Sie die Definition von NotEquals () für die Klasse öffnen, die Equals () implementiert, erzwingen Sie erneut das Problem der Konsistenz bei der Bestimmung der Gleichheit.

Edit: Das ist einfach meine Argumentation.

2
Emoire

Programmiersprachen sind syntaktische Umordnungen außergewöhnlich komplexer logischer Aussagen. Können Sie in diesem Sinne einen Fall der Gleichheit definieren, ohne einen Fall der Ungleichheit zu definieren? Die Antwort ist nein. Damit ein Objekt a gleich Objekt b ist, muss auch die Umkehrung von Objekt a gleich b wahr sein. Ein anderer Weg, dies zu zeigen, ist

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

dies gibt der Sprache die definitive Fähigkeit, die Gleichheit von Objekten zu bestimmen. Zum Beispiel kann der Vergleich NULL! = NULL einen Schlüssel in die Definition eines Gleichheitssystems werfen, das keine Ungleichheitsaussage implementiert.

Nun, in Bezug auf die Idee von! = Einfach austauschbare Definition wie in

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

Dem kann ich nicht widersprechen. Es war jedoch höchstwahrscheinlich eine Entscheidung der C # -Sprachenspezifikationsgruppe, dass der Programmierer gezwungen werden musste, die Gleichheit und die Ungleichheit eines Objekts gemeinsam explizit zu definieren

2
Josh