Informationen zum Vergleichen von Objekten nach Wert - 1: Anfang

    Es ist allgemein bekannt, dass Objekte im .NET- Objektmodell wie auf vielen anderen Softwareplattformen nach Referenz und nach Wert verglichen werden können.


    Standardmäßig werden zwei Objekte als gleich betrachtet, wenn die entsprechenden Variablen dieselbe Referenz enthalten. Andernfalls werden die Objekte als ungleich angesehen.

    Es kann jedoch vorkommen, dass Objekte einer bestimmten Klasse als gleich angesehen werden müssen, wenn sie in ihrem Inhalt auf eine bestimmte Weise übereinstimmen.

    Es soll eine Personenklasse geben, die personenbezogene Daten enthält - Name, Vorname und Geburtsdatum der Person.


    Betrachten Sie am Beispiel dieser Klasse:

    1. die mindestens erforderlichen Verbesserungen an der Klasse, damit Objekte dieser Klasse unter Verwendung der standardmäßigen .NET- Infrastruktur nach Wert verglichen werden ;
    2. die mindestens erforderlichen und ausreichenden Verbesserungen, damit Objekte dieser Klasse unter Verwendung der standardmäßigen .NET- Infrastruktur immer nach ihrem Wert verglichen werden - es sei denn, es wird eindeutig angegeben, dass der Vergleich anhand eines Verweises durchgeführt werden soll.

    In jedem Fall werden wir überlegen, wie es besser ist, einen Vergleich von Objekten nach Wert durchzuführen, damit wir einen konsistenten und möglichst kompakten, kopier- und fügungsfreien, produktiven Code erhalten.

    Die Aufgabe ist nicht so einfach, wie es auf den ersten Blick erscheinen mag.

    Überlegen Sie auch, welche Verbesserungen an der Plattform vorgenommen werden könnten, um die Implementierung dieser Aufgabe zu vereinfachen.

    Personenklasse:
    Klasse Person
    using System;
    namespace HelloEquatable
    {
        public class Person
        {
            protected static string NormalizeName(string name) => name?.Trim() ?? string.Empty;
            protected static DateTime? NormalizeDate(DateTime? date) => date?.Date;
            public string FirstName { get; }
            public string LastName { get; }
            public DateTime? BirthDate { get; }
            public Person(string firstName, string lastName, DateTime? birthDate)
            {
                this.FirstName = NormalizeName(firstName);
                this.LastName = NormalizeName(lastName);
                this.BirthDate = NormalizeDate(birthDate);
            }
        }
    }
    

    Wenn zwei Objekte der Klasse Person auf irgendeine Weise verglichen werden:
    Dann werden Objekte nur dann als gleich angesehen, wenn die Variablen, die auf sie zeigen, denselben Link enthalten.

    In Hash-Sets (Hash-Maps) und Wörterbüchern werden Objekte nur dann als gleich angesehen, wenn die Verknüpfungen übereinstimmen.

    Um Objekte im Client-Code nach Wert zu vergleichen, müssen Sie Zeilen des folgenden Formulars schreiben:
    Code
    var p1 = new Person("John", "Smith", new DateTime(1990, 1, 1));
    var p2 = new Person("John", "Smith", new DateTime(1990, 1, 1));
    bool isSamePerson =
        p1.BirthDate == p2.BirthDate &&
        p1.FirstName == p2.FirstName &&
        p1.LastName == p2.LastName;
    

    Anmerkungen:

    1. Die Person-Klasse ist so implementiert, dass die Zeichenfolgeneigenschaften von FirstName und LastName immer nicht null sind .
      Wenn Vorname oder Nachname unbekannt (nicht festgelegt) ist, eignet sich eine leere Zeichenfolge als Zeichen für das Fehlen eines Werts .
      Dies vermeidet eine NullReferenceException, wenn auf die Eigenschaften und Methoden der Felder FirstName und LastName verwiesen wird, sowie Kollisionen beim Vergleich von null und einer leeren Zeichenfolge (sollten zwei Objekte als gleich angesehen werden, wenn eines der FirstName-Objekte null ist und das andere eine leere Zeichenfolge enthält ?). .
    2. Die BirthDate-Eigenschaft wird dagegen als Nullable (Of T) -Struktur implementiert , weil falls das geburtsdatum unbekannt ist (nicht festgelegt), ist es ratsam, in der immobilie genau einen unbestimmten wert und keinen speziellen wert der form 01/01/1900, 01/01/1970, 01/01/0001 oder MinValue zu speichern .
    3. Beim Vergleichen von Objekten nach Wert wird zuerst das Datum verglichen, weil Das Vergleichen von Datentypvariablen ist im Allgemeinen schneller als das Vergleichen von Zeichenfolgen.
    4. Das Vergleichen von Daten und Zeichenfolgen wird mit dem Gleichheitsoperator "because" implementiert Der Gleichheitsoperator vergleicht Strukturen nach Wert, und für Zeichenfolgen ist der Gleichheitsoperator überladen und vergleicht Zeichenfolgen ebenfalls nach Wert.

    Damit Objekte der Klasse Person wie folgt nach ihrem Wert verglichen werden können:


    Für die Person-Klasse müssen Sie die Methoden Object.Equals (Object) und Object.GetHashCode () wie folgt überschreiben :

    • Die Methode Equals (Object) vergleicht die Felder der Klasse, deren Wertekombination den Wert des Objekts bildet.
    • Die GetHashCode () -Methode sollte für gleiche Objekte dieselben Hash-Codes zurückgeben (d. H. Für Objekte, deren Vergleich mit Equals (Object) true zurückgibt ).
      Daraus folgt, dass Objekte nicht gleich sind, wenn Objekte unterschiedliche Hash-Codes haben. Während ungleiche Objekte die gleichen Hash-Codes haben können.
      (Um einen Hash-Code zu erhalten, wird normalerweise das Ergebnis der Exklusiv- oder -Operation von GetHashCode () -Werten der Felder verwendet, die von Equals zum Vergleichen von Objekten nach Wert verwendet werden .
      In diesem Fall ist jedes Feld eine 32-Bit-Ganzzahl anstelle des Hash-Codes dieses Felds der Feldwert selbst kann verwendet werden;
      Es sind auch verschiedene Optimierungen möglich, um die Wahrscheinlichkeit von Kollisionen zu minimieren, wenn zwei ungleiche Objekte denselben Hash-Code haben.)

    Es ist besonders zu beachten, dass die Dokumentation der Equals (Object) -Methode besondere Anforderungen enthält:

    • x.Equals (y) gibt den gleichen Wert wie y.Equals (x) zurück.
    • Wenn (x.Equals (y) && y.Equals (z)) true zurückgibt, gibt x.Equals (z) true zurück.
    • x.Equals (null) gibt false zurück.
    • Aufeinanderfolgende Aufrufe von x.Equals (y) geben denselben Wert zurück, solange die Objekte, auf die x und y verweisen, nicht geändert werden.
    • Und einige andere, insbesondere in Bezug auf die Regeln zum Vergleichen der Werte von Gleitkommazahlen.

    Es ist auch zu beachten, dass die Dokumentation für die GetHashCode () -Methode eine Warnung enthält, dass der von der Methode zurückgegebene Wert kein konstanter Wert ist und daher nicht auf der Festplatte oder in der Datenbank gespeichert werden sollte, die als Schlüssel verwendet wird, und dass dies auch nicht der Fall ist sollte zum Vergleichen von Objekten verwendet werden (ungleiche Objekte können dieselben Hash-Codes haben) usw.

    Personenklasse mit überlappenden Equals (Object) - und GetHashCode () -Methoden :
    Klasse Person
    using System;
    namespace HelloEquatable
    {
        public class Person
        {
            protected static string NormalizeName(string name) => name?.Trim() ?? string.Empty;
            protected static DateTime? NormalizeDate(DateTime? date) => date?.Date;
            public string FirstName { get; }
            public string LastName { get; }
            public DateTime? BirthDate { get; }
            public Person(string firstName, string lastName, DateTime? birthDate)
            {
                this.FirstName = NormalizeName(firstName);
                this.LastName = NormalizeName(lastName);
                this.BirthDate = NormalizeDate(birthDate);
            }
            public override int GetHashCode() =>
                this.FirstName.GetHashCode() ^
                this.LastName.GetHashCode() ^
                this.BirthDate.GetHashCode();
            protected static bool EqualsHelper(Person first, Person second) =>
                first.BirthDate == second.BirthDate &&
                first.FirstName == second.FirstName &&
                first.LastName == second.LastName;
            public override bool Equals(object obj)
            {
                if ((object)this == obj)
                    return true;
                var other = obj as Person;
                if ((object)other == null)
                    return false;
                return EqualsHelper(this, other);
            }
        }
    }

    Hinweise zur GetHashCode () -Methode :

    • Wenn eines der verwendeten Felder null enthält , wird anstelle des Werts GetHashCode () normalerweise null verwendet.
    • Klasse Person in einer solchen Art und Weise durchgeführt , dass die Referenzfelder Vorname und Nachname nicht enthalten null , während das Feld Geburtsdatum ist Nullable (Of T) -Struktur, für die im Falle unklarer Signifikanz GetHashCode () gibt Null Ausnahmen Nullreferenceexception beim Aufruf GetHashCode () nicht entsteht.
    • Wenn die Felder der Person-Klasse null enthalten könnten , würde die GetHashCode () -Methode wie folgt implementiert:
      GetHashCode ()
      public override int GetHashCode() =>
          this.FirstName?.GetHashCode() ?? 0 ^
          this.LastName?.GetHashCode() ?? 0 ^
          this.BirthDate?.GetHashCode() ?? 0;
      

    Betrachten wir im Detail, wie die Equals (Object) -Methode implementiert ist :

    1. Zunächst wird die Verknüpfung mit dem aktuellen Objekt ( this ) mit der Verknüpfung mit dem eingehenden Objekt verglichen. Wenn die Verknüpfungen gleich sind, wird true zurückgegeben (dies ist dasselbe Objekt, und der Vergleich nach Wert ist auch aus Leistungsgründen nicht sinnvoll).
    2. Anschließend wird das Eingabeobjekt mit dem Operator as in den Typ Person umgewandelt . Wenn das Ergebnis der Umwandlung null ist , wird false zurückgegeben (entweder war der eingehende Link anfangs null , oder das eingehende Objekt ist von einem mit der Person-Klasse inkompatiblen Typ und entspricht offensichtlich nicht dem aktuellen Objekt).
    3. Anschließend werden die Felder zweier Objekte der Klasse Person nach Wert verglichen und das entsprechende Ergebnis zurückgegeben.
      Zur Lesbarkeit des Codes und zur möglichen Wiederverwendung wird der Objektvergleich direkt nach Wert in der EqualsHelper-Hilfsmethode ausgeführt.

    Bisher haben wir nur die minimal notwendige Funktionalität zum Vergleichen von Objekten nach Wert implementiert, aber es stellen sich bereits Fragen.


    Die erste Frage ist eher theoretisch.


    Beachten Sie die Anforderung für die Equals (Object) -Methode :
    x.Equals(null) returns false.

    Ich war einmal daran interessiert, warum einige Instanzmethoden in der .NET-Standardbibliothek dies auf null überprüfen - zum Beispiel ist die String.Equals (Object) -Methode folgendermaßen implementiert :
    String.Equals (Objekt)
    public override bool Equals(Object obj) {
        //this is necessary to guard against reverse-pinvokes and
        //other callers who do not use the callvirt instruction
        if (this == null)
            throw new NullReferenceException();
        String str = obj as String;
        if (str == null)
            return false;
        if (Object.ReferenceEquals(this, obj))
            return true;
        if (this.Length != str.Length)
            return false;
         return EqualsHelper(this, str);
    }
    

    Первым делом в методе выполняется проверка this на null и, в случае положительного результата проверки, генерируется исключение NullReferenceException.

    В комментарии указано, в каких случаях this может принимать null-значение.

    (Кстати, сравнение this на null выполнено с помощью оператора ==, который у класса Stringперегружен, поэтому с точки зрения производительности проверку лучше сделать, явно приведя this к object: (object)this == null, или же воспользоваться методом Object.ReferenceEquals(Object, Object), как это сделано во втором сравнении в этом же методе.)

    А затем появилась статья, где об этом можно прочитать подробнее: Когда this == null: невыдуманная история из мира CLR.

    Однако, в таком случае, если вызвать перегруженный метод Person.Equals(Object) без создания экземпляра, передав в качестве входного параметра null, то первая же строчка метода (if ((object)this == obj) return true;) возвратит true, что фактически будет правильно, но формально будет противоречить требованиям к реализации метода.

    При этом в документации к методу не указано, что первым делом нужно проверять this на nullund eine Ausnahme auslösen, wenn die Validierung erfolgreich ist.

    Und in diesem Fall wäre es sinnvoll, dies in allen Instanzmethoden aller Klassen mit der ersten Zeile auf null zu prüfen , was absurd ist.

    Daher sollten die offiziellen Anforderungen für die Implementierung der Equals (Object) -Methode wie folgt geklärt werden:

    • (für Klassen, nicht Strukturen) Wenn die Verweise auf das aktuelle und das eingehende Objekt gleich sind, wird true zurückgegeben .
    • und schon die zweite Voraussetzung - wenn der Verweis auf das eingehende Objekt null ist , wird false zurückgegeben .

    Die zweite Frage zur Implementierung der Equals (Object) -Methode ist jedoch interessanter und hat sich bewährt.


    Es geht darum, wie die Anforderung am besten umgesetzt werden kann:
    x.Equals(y) returns the same value as y.Equals(x).
    Und ob die Anforderungen und Beispiele für die Implementierung der Methode in diesem Teil vollständig und konsistent in der Dokumentation sind und ob es alternative Ansätze für die Implementierung dieser Anforderung gibt.

    In den folgenden Veröffentlichungen werden wir darüber sprechen sowie Fragen zur Implementierung einer vollständigen Reihe von Klassenverfeinerungen zum Vergleichen der Objekte nach Wert .


    Jetzt auch beliebt: