Tracks
/
C#
C#
/
Lehrplan
/
Gleichheit
Gl

Gleichheit in C#

5 Übungen

Über Gleichheit

Die Übung veranschaulicht eine Reihe von Eigenschaften der Gleichheit in C#:

Object.Equals()

Von der Übung behandelte Themen

  • Einfache Typen (Strings und primitive Typen) werden üblicherweise mit == und != auf Gleichheit geprüft. Das gilt als idiomatischer als die Verwendung der Methode Equals(), die es für diese Typen ebenfalls gibt. Java-Programmierer sollten bei Strings darauf achten, dass == in C# nach dem Wert vergleicht, in Java dagegen nach der Referenz, wenn sie zu ihrer früheren Sprache zurückkehren.
  • Referenztypen (Instanzen von Klassen) werden mit der von object geerbten Methode Equals() verglichen. Wenn es dir beim Gleichheitstest darum geht, sicherzustellen, dass zwei Objekte genau dieselbe Instanz sind, dann genügt die Implementierung von object. Wenn nicht, musst du object.Equals() überschreiben.
  • Wenn du weißt, dass alle Instanzen deiner Klasse an einer Stelle erzeugt werden, etwa die Figuren in einem Spiel oder einer Simulation, dann genügt Referenzgleichheit. Es ist jedoch wahrscheinlich, dass mehrere Instanzen derselben realen Entität erzeugt werden (zum Beispiel aus einer Datenbank, durch Benutzereingaben oder über eine Webanfrage). In diesem Fall müssen die Werte, die die Entität eindeutig identifizieren, auf Gleichheit geprüft werden. Deshalb muss Equals() überschrieben werden.
  • Ein überschriebenes Equals() enthält Gleichheitstests für Member einfacher Typen mit == und für Referenztypen mit rekursiven Aufrufen von Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • Die statische Methode object.ReferenceEquals() wird verwendet, um zwei Objekte zu vergleichen und festzustellen, ob sie ein und dieselbe Instanz sind. Das sorgt für Klarheit und ist notwendig, wenn Equals() und == überschrieben bzw. überladen wurden.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

Ergänzende Themen

  • Zusätzlich zu public override bool Equals(object obj) erzeugen IDEs normalerweise die Überladung protected bool Equals(FacialFeatures other), die bei Vererbung zum Einsatz kommt. Eine abgeleitete Klasse kann Equals() der Basisklasse aufrufen und dann ihren eigenen Test hinzufügen.
  • Verwende == nur, wenn du den ==-Operator und auch die Methode Equals() in deiner Klasse überladen hast (siehe die Übung operator-overloading) oder wenn es dir nur darum geht, dass die Referenzen gleich sind.
  • Gleichheitstests bei structs werden in der Übung structs behandelt.
  • Die Gleichheit von tuples wird in der Übung tuples behandelt.
  • Viele Entwickler verlassen sich darauf, dass ihre IDEs die Implementierung der Gleichheitsmethoden liefern, denn diese kümmern sich um alle Feinheiten der Gleichheit. Zum Beispiel erzeugt JetBrains' RIDER (v 2020.1) für eine Klasse die folgenden Gleichheitsmethoden:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • Sei vorsichtig, wenn du den von deiner IDE erzeugten Code verbessern möchtest. Dieser Code ist im Allgemeinen wasserdicht gegen Nulls und falsch typisierte Objekte und ziemlich gut optimiert.
  • Tests auf die Gleichheit von Delegaten werden in dieser Übung nicht eigens behandelt.
  • Für Arrays und die meisten Sammlungen gibt es keine eingebauten Gleichheitstests. LINQ (wird in späteren Übungen behandelt) bietet SequenceEquals(), aber ohne LINQ bleibt nur, beide Sammlungen zu durchlaufen und die Elemente einzeln zu vergleichen.
  • Wie du == und != mit deinen eigenen Klassen verwendest, wird in der Übung operator-overloading besprochen.

Object.GetHashCode()

  • object.GetHashCode() gibt einen Hashcode in Form einer 32-Bit-Ganzzahl zurück. Der Hashcode wird von Wörterbuch- und Set-Klassen wie Dictionary<T> und HashSet<T> verwendet, um Objekte performant zu speichern und abzurufen. Bei Wörterbüchern bezieht sich das Hashing auf die Schlüssel.
  • Unter C#-Entwicklern wird erwartet, dass du GetHashCode() ebenfalls überschreibst, wenn du Equals() überschreibst. Zwischen Equals() und GetHashCode() besteht eine Beziehung, die gelten muss, damit Wörterbuch- und HashSet-Klassen und alle anderen, die einen Hashcode verwenden, korrekt funktionieren. Du solltest die Methode so implementieren, dass du Maintainern keine Fallen stellst, die später eine hashcodebasierte Sammlung hinzufügen.
  • Die Beziehung zwischen Hashcode und Gleichheit ist: Wenn zwei Objekte gleich sind (Equal() gibt wahr zurück), dann müssen GetHashCode() für die beiden Objekte denselben Wert zurückgeben. Umgekehrt gilt das nicht. Die Beziehung ist nicht symmetrisch. Stell dir eine Suchfunktion vor, die anhand des Hashcodes zuerst einen „Bucket“ ansteuert und dann das jeweilige Element über den Gleichheitstest herausgreift.
  • Der einfachste Weg, einen Hashcode zu erzeugen, ist der Aufruf von HashCode.Combine(), dem du die im Gleichheitstest verwendeten Werte (oder eine Teilmenge davon) übergibst. Bedenke: Je mehr Informationen du Combine() gibst, desto performanter ist die Hash-Implementierung wahrscheinlich.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • Die im Gleichheitstest verwendeten Werte müssen stabil bleiben, solange die hashbasierte Sammlung verwendet wird. Wenn du ein Objekt mit einem Satz von Werten zur Sammlung hinzufügst und diese Werte dann änderst, zeigt der Hashcode nicht mehr auf den richtigen „Bucket“. In der Praxis bedeutet das, dass das Objekt unveränderlich sein sollte. Andere Ansätze bergen das Risiko, Maintainern Stolperfallen zu stellen. Unveränderlichkeit wird in anderen Übungen behandelt.
  • Es ist möglich, dass du einen besseren Hashcode entwirfst als den, den die Bibliotheksroutinen erzeugen. Aber das gelingt dir entweder, weil du die Eigenschaften der Daten im Detail kennst, oder weil die Sammlung sehr einfach ist und die Werte direkt ohne Hashing verwendet werden können. Der zusätzliche Aufwand lohnt sich vielleicht nicht.

Leistungsverbesserungen

Um die Leistung etwas zu verbessern, besonders wenn Objekte zu Sammlungen gehören, kannst du ein überladenes Member public bool Equals(T other) hinzufügen.

Das spart eine gewisse Menge an Typprüfungen für Referenztypen und spart einen Boxing-Schritt für Werttypen, da diese nicht in ein Objekt umgewandelt (geboxt) werden müssen, um als Argument an public override bool Equals(object other) übergeben zu werden.

Wenn du das Interface IEquatable<T> zu deiner Klasse hinzufügst, muss die Überladung implementiert werden. Wenn dein Code keine Routinen enthält, die Objekte vom Typ IEquatable<T> entgegennehmen (und damit vermutlich etwas Interessantes rund um Gleichheit tun, unabhängig von der implementierenden Klasse), gibt es keinen anderen zwingenden Grund, das Interface einzubinden.

IEqualityComparer<T>

Wenn du eine Klasse hast, die auf zwei verschiedene Arten eindeutig identifiziert werden kann, etwa eine Person-Klasse mit einer SSID und einer eindeutigen E-Mail-Adresse, dann bietet .NET eine Möglichkeit, zwei verschiedene Sammlungen unterschiedliche Hashcode- und Gleichheitstests verwenden zu lassen. Jede kann eine andere Implementierung von IEqualityComparer<T> mit eigener Equals()- und GetHashCode()-Methode haben. Du kannst ein Wörterbuch mit dem Schlüssel SSID und ein weiteres mit dem Schlüssel E-Mail-Adresse haben.

Wenn IEqualityComparer<T> im Spiel ist, solltest du in deiner Elementklasse trotzdem Equals() und GetHashCode() überschreiben, um Probleme außerhalb der Sammlungsklassen zu vermeiden.

Ein Aspekt bei der Verwendung von IEqualityComparer<T> ist, dass private Methoden usw. für den Gleichheitstest nicht zur Verfügung stehen.

Wenn nur eine hashbasierte Sammlung im Spiel ist, ist es vielleicht besser, IEqualityComparer<T> zu vermeiden und Gleichheit und Hashcode im Objekt selbst zu kapseln. Es ist ohnehin nicht ideal, dass eine so wichtige Abhängigkeit wie die zwischen Objekt und hashbasierter Sammlung nicht vom Compiler erzwungen werden kann. Wenn die Hashing- und Vergleichslogik aber an anderer Stelle liegt, wäre es für einen Maintainer noch leichter, die Member einer Klasse zu ändern, ohne die Folgen für die Sammlung zu bedenken.

Hinweis zur Gleichheit von Gleitkommazahlen

Ein primitiver Typ, der unvorsichtige Programmierer herausfordern kann, ist der Test auf Gleichheit von Gleitkommawerten. Das wird im about.md-Dokument für das Konzept floating-point-numbers behandelt.

Gleichheit und Vererbung

Dieser Artikel zeigt einige der Entscheidungen, die bei Gleichheit und Vererbung getroffen werden müssen. Sie betreffen dich nur, wenn du mit Instanzen einer Klasse und einer abgeleiteten Klasse arbeitest und die Gleichheit zwischen beiden testen musst.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne Gleichheit

Das Üben ist gesperrt

Schalte 4 weitere Übungen frei, um Gleichheit zu üben