Kolumne: C# im Fokus

Richtlinien für das Überschreiben von Equals GetHashCode und ToString
Kommentare

Während der Umsetzung von eigenen Anwendungen werden typischerweise eigene Klassen definiert. Jede Klasse, auch wenn keine Oberklasse angegeben wird, erbt die Eigenschaften der .NET-Wurzelklasse Object. Die Klasse Object besitzt drei wesentliche Methoden, die es korrekt zu implementieren gilt.

Wie in der Einleitung angedeutet wurde, erbt jede Klasse direkt oder indirekt von der Klasse Object. Die Klasse Objekt stellt somit die absolute Oberklasse aller eingebauten Framework- und selbst definierten .NET-Klassen dar. Die Klasse Object besitzt drei Methoden, die in selbst definierten Klassen korrekt überschrieben werden sollten. Konkret handelt es sich um die folgenden Methoden:

  • Equals
  • GetHasCode
  • ToString

Diese Ausgabe der Kolumne erläutert die Implementierung der Equals-Methode, die Ausgabe 12.2010 des dot.NET Magazins widmet sich der GetHashCode-Methode und ein abschließender dritter Teil erläutert die ToString-Methode und demonstriert anhand eines fachlichen Objektmodells das Zusammenspiel der drei Methoden. Es sei aber schon an dieser Stelle darauf hingewiesen, dass nicht zwingend jede eigene Klasse die genannten Methoden überschreiben muss. Ob eine Überschreibung notwendig wird, hängt vom Typ der Klasse ab. Unterschieden werden kann hierbei zwischen Funktions- und Entitätsklassen.

Funktionsklassen

Bei Funktionsklassen handelt es sich um Klassen, die hauptsächlich (geschäftsprozess-spezifische) Funktionen bereitstellen. Oft besitzen diese Klassen eine Vielzahl an statischen (static) Methoden. Über Funktionsklassen werden allgemeine Funktionen zur Verfügung gestellt, die von Entitätsklassen bzw. Fachklassen verwendet werden. Auch sog. Werkzeugklassen (Utility classes) können diesem Klassentyp zugeordnet werden. Im Gegensatz zu fachlichen Funktionsklassen stellen jedoch Werkzeugklassen verschiedene nicht zusammengehörige Funktionen bereit, die an keine bestimmte Entitätsklasse gebunden sind (z. B. die Klasse SPUtility).

Entitätsklassen

Bei Entitätsklassen handelt es sich um Klassen, die einen konkreten Ausschnitt aus der fachlich abzubildenden Welt darstellen. Bei der Realisierung eines CRM (Customer Relationship Management) Systems stellt z. B. die Klasse Person oder Kunde eine solche fachliche Klasse dar. Bei diesen Klassentypen ist es wichtig, die vom Objekt geerbten Methoden korrekt zu definieren. Generell kann festgelegt werden, dass alle Typen von Klassen die entsprechenden Object-Methoden implementieren müssen, wenn deren spätere Objekte einer Auflistungsklasse (Hashtable, Collection) hinzugefügt werden sollen. Unabhängig davon ist die korrekte Implementierung der Equals-Methode für eine fehlerfreie Funktion unabdingbar. Anhand einer einfachen Klasse wird im Folgenden demonstriert, wie die Methoden zu überschreiben sind und welche Probleme entstehen können, wenn dies nicht durchgeführt wird. Für die nachfolgenden Beispiele wird die in Abbildung 1 dargestellte Personenklasse verwendet.

Abb. 1: Die Beispielklasse
Abb. 1: Die Beispielklasse „Person“

Die Standardimplementierung von Equals

Die Methode Equals ist innerhalb der Object-Klasse definiert, und somit erben alle .NET-Klassen diese Methode. Leider ist diese funktionell genauso definiert wie die Vergleichsoperatoren ==. Die Operatoren vergleichen bei Referenztypen (Reference Types) lediglich die Referenzen auf Gleichheit und berücksichtigen nicht den konkreten Inhalt des Objekts. Bei Wertetypen (Value Types) greift allerdings ein anderer Mechanismus, wie weiter unter beschrieben wird. Da die Standardimplementierung von Equals sich bei dem Vergleich auf die Referenzen bezieht, kann dies für fachliche Entitätsklassen problematisch werden.

Die Equals-Methode

Die korrekte Implementierung der Equals-Methode ist existenziell, um die logisch fehlerfreie Funktion einer Anwendung zu gewährleisten (Listing 1).

public static void Example1()
{
  Person person1 = new Person() {
    UniqueID = 1,Salutation = "Herr",
    FirstName = "Hans", LastName = "Schmitt"
  };
 Person person2 = new Person() {
    UniqueID = 1, Salutation = "Herr",
    FirstName = "Hans", LastName = "Schmitt"
  };
  Console.WriteLine("Equals Ergebnis: {0}", person1.Equals(person2));
}  

Erzeugt werden zwei Objekte vom Typ Person und mit gleichen Werten initialisiert. Aus fachlicher Sicht handelt es sich um die gleichen Personen, da auch die Eigenschaft UniqueID – dabei könnte es sich um einen Datenbankschlüssel handeln – den gleichen Wert besitzt. Wird das Beispiel ausgeführt, liefert die Methode Equals jedoch false statt true zurück. Technisch gesehen verhält sich die Methode korrekt, da in der Standardimplementierung der Equals-Methode nicht die Inhalte, sondern die Objektreferenzen (Speicheradressen) verglichen werden, und diese sind bei zwei unabhängigen Objekten natürlich unterschiedlich. Um ein fachlich korrektes Ergebnis zu erzielen, muss die Methode entsprechend überschrieben werden. Listing 2 demonstriert die korrekte und vollständige Implementierung der Equals-Methode innerhalb der Person-Klasse.

class Person {
  public int UniqueID { get; set; }
  public string Salutation { get; set; }
  public string LastName { get; set; }
  public string FirstName { get; set; }

  public override bool Equals(object obj) {
    if (obj == null) return false;
    if ((obj as Person) == null) return false;
    if (Object.ReferenceEquals(this, obj)) return true;
    Person toCompare = obj as Person;
    return (toCompare.UniqueID == this.UniqueID
            && toCompare.FirstName.Equals(this.FirstName)
            && toCompare.LastName.Equals(this.LastName));
  }  

Anhand der Beispielimplementierung können folgende Regeln identifiziert werden, die bei der Umsetzung eingehalten werden müssen:

Object.ReferenceEquals vs. ==

Im Punkt 3 des vorherigen Abschnitts wurde bereits darauf hingewiesen, dass auf die Verwendung von == zum Vergleich auf Objektgleichheit verzichtet werden sollte. Da unter Umständen die Operatoren überschrieben wurden und somit die Referenzen eventuell nicht mehr auf Gleichheit prüfen, werden fehlerhafte Ergebnisse erzielt. Anstatt die ==-Operatoren anzuwenden, kommt die statische Methode ReferenceEquals der Object-Klasse zum Einsatz. Diese Methode prüft, wie erwartet, ob die Referenz der übergebenen Objekte übereinstimmt. Alternativ können auch die Objekte zunächst auf die Klasse Object gecastet werden und dann mittels == verglichen werden.

ValueType vs. ReferenceType

Wie bereits angedeutet wurde, verhält sich die Standardimplementierung der Equals-Methode für Wertetypen anders. Hier prüft die Equals-Methode alle enthaltenen Felder des Wertetyps (ValueTypes) auf Gleichheit. Intern ist die Methode wie folgt umgesetzt:

  1. Die Methode testet zunächst, ob der übergebene Vergleichsparameter null ist. Wenn ja, liefert die Methode false zurück.
  2. Danach wird geprüft, ob die gleichen Speicherstellen verwendet werden. Wenn ja, liefert die Methode true zurück.
  3. Im letzten Schritt werden alle enthaltenen Feldwerte mit dem übergebenen Werteparameter verglichen. Sind alle Werte gleich, liefert die Methode true zurück.

Das heißt: wird die in Listing 1 gezeigte Personenklasse zu einem Wertetyp umgewandelt, ist keine eigene Überschreibung der Methode Equals notwendig. Listing 3 zeigt die notwendigen Änderungen.

struct PersonVT {
  public int UniqueID { get; set; }
  public string Salutation { get; set; }
  public string LastName { get; set; }
  public string FirstName { get; set; }
}
public static void Example2()
{
  PersonVT person1 = new PersonVT() {
    UniqueID = 1, Salutation = "Herr",
    FirstName = "Hans",LastName = "Schmitt" };
  PersonVT person2 = new PersonVT() {
    UniqueID = 1,Salutation = "Herr",
    FirstName = "Hans",LastName = "Schmitt" };
  Console.WriteLine("Equals Ergebnis: {0}", person1.Equals(person2));
}  

Wird das veränderte Beispiel ausgeführt, liefert der Aufruf von Equals – wie erwartet – true zurück. Zu beachten ist allerdings, dass im dritten Punkt der beschriebenen Prüfung Reflection verwendet wird, um die Feldwerte zu ermitteln. Dies könnte unter Umständen zu Performanceproblemen führen.

Weitere Regeln

Zusätzlich zu den bisher erläuterten Implementierungsregeln muss eine konforme Equals-Methode folgende Bedingungen erfüllen:

  • Equals muss reflexiv aufrufbar sein, das heißt, der Aufruf obj.Equals(obj) muss true ergeben.
  • Die Aufrufe a.Equals(b) und b.Equals(a) müssen das gleiche Ergebnis liefern.
  • Jeder Aufruf von Equals muss bei unveränderten Objekten das gleiche Ergebnis produzieren.
  • Equals muss transitiv umgesetzt werden. Das bedeutet, wenn a.Equals(b) true liefert und b.Equals(c) true ergibt, muss auch a.Equals(c) true ergeben.

Ausblick

Im nächsten Teil der C#-Kolumne wird auf die Methode GetHashCode eingegangen. Wie bei der Methode Equals müssen auch bei dieser Methode einige Regeln eingehalten werden, um diese .NET-Framework-konform zu realisieren.

    Marc André Zhou arbeitet als Senior Consultant bei der Logica Deutschland GmbH & Co. KG. Seine Schwerpunkte liegen im Bereich Softwarearchitekturen und Frameworks, hier hauptsächlich im .NET-Umfeld.
    Unsere Redaktion empfiehlt:

    Relevante Beiträge

    Meinungen zu diesem Beitrag

    X
    - Gib Deinen Standort ein -
    - or -