Überladen von Operatoren mit schnittstellenbasierter Programmierung in C #

73

Hintergrund

Ich verwende die schnittstellenbasierte Programmierung für ein aktuelles Projekt und bin beim Überladen von Operatoren (insbesondere der Operatoren für Gleichheit und Ungleichheit) auf ein Problem gestoßen.


Annahmen

  • Ich verwende C # 3.0, .NET 3.5 und Visual Studio 2008

UPDATE - Die folgende Annahme war falsch!

  • Das Erfordernis aller Vergleiche zur Verwendung von Equals anstelle von operator == ist keine praktikable Lösung, insbesondere wenn Sie Ihre Typen an Bibliotheken (z. B. Sammlungen) übergeben.

Der Grund, warum ich besorgt war, dass Equals anstelle von operator == verwendet werden muss, ist, dass ich in den .NET-Richtlinien nirgendwo finden konnte, dass Equals anstelle von operator == verwendet oder sogar vorgeschlagen wird. Nach dem erneuten Lesen der Richtlinien zum Überschreiben von Gleichheit und Operator == habe ich Folgendes gefunden:

Standardmäßig prüft der Operator == die Referenzgleichheit, indem er ermittelt, ob zwei Referenzen dasselbe Objekt angeben. Daher müssen Referenztypen den Operator == nicht implementieren, um diese Funktionalität zu erhalten. Wenn ein Typ unveränderlich ist, dh die in der Instanz enthaltenen Daten nicht geändert werden können, kann das Überladen des Operators == zum Vergleichen der Wertgleichheit anstelle der Referenzgleichheit nützlich sein, da sie als unveränderliche Objekte als gleich lange betrachtet werden können da sie den gleichen Wert haben. Es ist keine gute Idee, operator == in nicht unveränderlichen Typen zu überschreiben.

und diese gleichwertige Schnittstelle

Die IEquatable-Schnittstelle wird von generischen Auflistungsobjekten wie Dictionary, List und LinkedList verwendet, wenn die Gleichheit in Methoden wie Contains, IndexOf, LastIndexOf und Remove getestet wird. Es sollte für jedes Objekt implementiert werden, das möglicherweise in einer generischen Sammlung gespeichert ist.


Einschränkungen

  • Für jede Lösung muss es nicht erforderlich sein, die Objekte von ihren Schnittstellen auf ihre konkreten Typen zu werfen.

Problem

  • Immer wenn beide Seiten des Operators == eine Schnittstelle sind, stimmt keine Signatur der Operator == -Überladungsmethode der zugrunde liegenden konkreten Typen überein, und daher wird die Standardmethode Object operator == aufgerufen.
  • Beim Überladen eines Operators für eine Klasse muss mindestens einer der Parameter des binären Operators der enthaltende Typ sein, andernfalls wird ein Compilerfehler generiert (Fehler BC33021 http://msdn.microsoft.com/en-us/library/watt39ff .aspx )
  • Es ist nicht möglich, die Implementierung auf einer Schnittstelle anzugeben

Siehe Code und Ausgabe unten, um das Problem zu demonstrieren.


Frage

Wie stellen Sie bei Verwendung der Schnittstellenbasisprogrammierung die richtigen Operatorüberladungen für Ihre Klassen bereit?


Verweise

== Operator (C # -Referenz)

Für vordefinierte Werttypen gibt der Gleichheitsoperator (==) true zurück, wenn die Werte seiner Operanden gleich sind, andernfalls false. Für andere Referenztypen als Zeichenfolge gibt == true zurück, wenn sich die beiden Operanden auf dasselbe Objekt beziehen. Für den Zeichenfolgentyp vergleicht == die Werte der Zeichenfolgen.


Siehe auch


Code

using System;

namespace OperatorOverloadsWithInterfaces
{
    public interface IAddress : IEquatable<IAddress>
    {
        string StreetName { get; set; }
        string City { get; set; }
        string State { get; set; }
    }

    public class Address : IAddress
    {
        private string _streetName;
        private string _city;
        private string _state;

        public Address(string city, string state, string streetName)
        {
            City = city;
            State = state;
            StreetName = streetName;
        }

        #region IAddress Members

        public virtual string StreetName
        {
            get { return _streetName; }
            set { _streetName = value; }
        }

        public virtual string City
        {
            get { return _city; }
            set { _city = value; }
        }

        public virtual string State
        {
            get { return _state; }
            set { _state = value; }
        }

        public static bool operator ==(Address lhs, Address rhs)
        {
            Console.WriteLine("Address operator== overload called.");
            // If both sides of the argument are the same instance or null, they are equal
            if (Object.ReferenceEquals(lhs, rhs))
            {
                return true;
            }

            return lhs.Equals(rhs);
        }

        public static bool operator !=(Address lhs, Address rhs)
        {
            return !(lhs == rhs);
        }

        public override bool Equals(object obj)
        {
            // Use 'as' rather than a cast to get a null rather an exception
            // if the object isn't convertible
            Address address = obj as Address;
            return this.Equals(address);
        }

        public override int GetHashCode()
        {
            string composite = StreetName + City + State;
            return composite.GetHashCode();
        }

        #endregion

        #region IEquatable<IAddress> Members

        public virtual bool Equals(IAddress other)
        {
            // Per MSDN documentation, x.Equals(null) should return false
            if ((object)other == null)
            {
                return false;
            }

            return ((this.City == other.City)
                && (this.State == other.State)
                && (this.StreetName == other.StreetName));
        }

        #endregion
    }

    public class Program
    {
        static void Main(string[] args)
        {
            IAddress address1 = new Address("seattle", "washington", "Awesome St");
            IAddress address2 = new Address("seattle", "washington", "Awesome St");

            functionThatComparesAddresses(address1, address2);

            Console.Read();
        }

        public static void functionThatComparesAddresses(IAddress address1, IAddress address2)
        {
            if (address1 == address2)
            {
                Console.WriteLine("Equal with the interfaces.");
            }

            if ((Address)address1 == address2)
            {
                Console.WriteLine("Equal with Left-hand side cast.");
            }

            if (address1 == (Address)address2)
            {
                Console.WriteLine("Equal with Right-hand side cast.");
            }

            if ((Address)address1 == (Address)address2)
            {
                Console.WriteLine("Equal with both sides cast.");
            }
        }
    }
}

Ausgabe

Address operator== overload called
Equal with both sides cast.
Zach Burlingame
quelle
Können Sie Ihre zweite Annahme näher erläutern? Auflistungsklassen sollten die Methode .Equals () verwenden.
KVB
7
+1 für Klarheit und Details in Frage.
Cyril Gupta
kvb - Ich habe meine zweite Annahme aktualisiert, und nachdem ich Johns Antwort und einige weitere MSDN-Dokumente gelesen habe, ist die Annahme falsch. Ich habe es oben notiert. Vielen Dank! Cyril - danke!
Zach Burlingame

Antworten:

57

Kurze Antwort: Ich denke, Ihre zweite Annahme könnte fehlerhaft sein. Equals()ist der richtige Weg, um die semantische Gleichheit zweier Objekte zu überprüfen , nicht operator ==.


Lange Antwort: Die Überlastungsauflösung für Operatoren wird zur Kompilierungszeit und nicht zur Laufzeit durchgeführt .

Solange der Compiler die Typen der Objekte, auf die er einen Operator anwendet, nicht definitiv kennt, wird er nicht kompiliert. Da der Compiler nicht sicher sein kann, ob IAddressein Objekt überschrieben werden soll ==, wird auf die Standardimplementierung operator ==von zurückgegriffen System.Object.

Versuchen Sie, eine operator +for zu definieren Addressund zwei IAddressInstanzen hinzuzufügen , um dies klarer zu sehen . Sofern Sie nicht explizit darauf setzen Address, kann es nicht kompiliert werden. Warum? Weil der Compiler nicht erkennen kann, dass es sich bei einer bestimmten IAddressum eine handelt Address, und es keine Standardimplementierung gibt operator +, auf die zurückgegriffen werden kann System.Object.


Ein Teil Ihrer Frustration beruht wahrscheinlich auf der Tatsache, dass ein Objectimplementiert operator ==wird und alles ein ist Object, sodass der Compiler Operationen wie a == bfür alle Typen erfolgreich auflösen kann . Wenn Sie überschrieben haben ==, haben Sie erwartet, dass Sie dasselbe Verhalten sehen, aber nicht, und das liegt daran, dass die beste ObjectImplementierung die ursprüngliche Implementierung ist, die der Compiler finden kann .

Das Erfordernis aller Vergleiche zur Verwendung von Equals anstelle von operator == ist keine praktikable Lösung, insbesondere wenn Sie Ihre Typen an Bibliotheken (z. B. Sammlungen) übergeben.

Meiner Ansicht nach sollten Sie genau das tun. Equals()ist der richtige Weg, um die semantische Gleichheit zweier Objekte zu überprüfen . Manchmal ist semantische Gleichheit nur Referenzgleichheit. In diesem Fall müssen Sie nichts ändern. In anderen Fällen, wie in Ihrem Beispiel, überschreiben EqualsSie, wenn Sie einen stärkeren Gleichstellungsvertrag als die Referenzgleichheit benötigen. Beispielsweise möchten Sie möglicherweise zwei als Personsgleich betrachten, wenn sie dieselbe Sozialversicherungsnummer haben, oder zwei als Vehiclesgleich, wenn sie dieselbe Fahrgestellnummer haben.

Aber Equals()und operator ==sind nicht dasselbe. Wann immer Sie überschreiben müssen operator ==, sollten Sie überschreiben Equals(), aber fast nie umgekehrt. operator ==ist eher eine syntaktische Bequemlichkeit. In einigen CLR-Sprachen (z. B. Visual Basic.NET) können Sie den Gleichheitsoperator nicht einmal überschreiben.

John Feminella
quelle
1
Richtig, nach dem erneuten Lesen von zwei der MSDN-Dokumente ist Equals die richtige Methode, um die Wertgleichheit zu überprüfen, während operator == für die Referenzgleichheit steht, es sei denn, es handelt sich möglicherweise um einen unveränderlichen Typ. Ich habe die Notation für die zweite Annahme mit den Dokumenten aktualisiert, aus denen hervorgeht, dass meine Annahme falsch ist. Vielen Dank!
Zach Burlingame
3
Froh, dass ich helfen konnte. Kurzer Hinweis: Ich verwende lieber den Ausdruck "semantische Gleichheit" als "Wertgleichheit", da zwei Objekte unterschiedliche Werte haben können, aber dennoch als gleich angesehen werden (z. B. zwei Postanschriften, bei denen "St" und "St" verwendet werden) andere Verwendungen "Straße").
John Feminella
Guter Punkt! Ich habe mich gerade daran gewöhnt, Wert und Referenz zu verwenden, da MSDN dies verwendet. Das heißt nicht, dass das, was MSDN verwendet, immer am besten oder sogar korrekt ist.
Zach Burlingame
Versteh mich nicht falsch - Wertgleichheit ist definitiv ein akzeptierter Begriff. Ich möchte nur die Unterscheidung klarer machen, wo ich kann.
John Feminella
2
Tatsächlich erlaubt VB.NET das Überschreiben des Gleichheitsoperators ( msdn.microsoft.com/en-us/library/ms379613%28VS.80%29.aspx ) und ist eine bessere Sprache, da dies Isanstelle der Referenzgleichheit verwendet wird Teilen =wie C #.
Gideon Engelberth
4

Wir sind auf dasselbe Problem gestoßen und haben eine hervorragende Lösung gefunden: Benutzerdefinierte Muster neu schärfen.

Wir haben ALLE unsere Benutzer so konfiguriert, dass sie zusätzlich zu ihrem eigenen einen gemeinsamen globalen Musterkatalog verwenden, und ihn in SVN abgelegt, damit er für alle versioniert und aktualisiert werden kann.

Der Katalog enthielt alle Muster, von denen bekannt ist, dass sie in unserem System falsch sind:

$i1$ == $i2$ (wobei i1 und i2 Ausdrücke unseres Schnittstellentyps sind oder abgeleitet werden.

Das Ersetzungsmuster ist

$i1$.Equals($i2$)

und der Schweregrad ist "Als Fehler anzeigen".

Ebenso haben wir $i1$ != $i2$

Hoffe das hilft. PS Global Catalogs ist die Funktion in Resharper 6.1 (EAP) und wird in Kürze als endgültig markiert.

Update : Ich habe ein Resharper-Problem eingereicht , um alle Schnittstellen '==' als Warnung zu markieren, es sei denn, es wird mit null verglichen. Bitte stimmen Sie ab, wenn Sie der Meinung sind, dass dies eine würdige Funktion ist.

Update2 : Resharper hat auch das Attribut [CannotApplyEqualityOperator], das helfen kann.

Yurik
quelle
+1 Für das Attribut [CannotApplyEqualityOperator] nützlich für Schnittstellen, bei denen die Referenzgleichheit wahrscheinlich nicht hilfreich ist.
JoshSub