Porównywanie obiektów

W C# istnieje kilka metod sprawdzania czy obiekty są takie same. Czasami budzi to zamieszanie i dlatego w dzisiejszym wpisie chciałbym rozjaśnić wszelkie różnice i wyjaśnić jak prawidłowo powinno to być zaimplementowane.

Mamy dwa sposoby porównywania obiektów. Pierwszy to sprawdzenie czy wskazują na taki sam obszar pamięci. Druga metoda polega na porównaniu wszystkich pól i sprawdzeniu czy są takie same. Jeśli pola mają taką samą wartość to przyjmujemy, że obiekty są sobie równe (value identity).

W .NET Framework metoda to object.Equals ma na celu porównanie typu value identity. Zachowuje ona się tak naprawdę dziwnie ponieważ domyślna implementacja sprawdza czy obiekty są sobie równe pod kątem wskaźnika  a nie wartości.

Wzorowa implementacja Equals (value identity), zgodna z przyjętymi praktykami powinna wyglądać następująco:

  1. Jeśli obiekt obj jest NULL wtedy zwróć false – mamy oczywiście tutaj pewność, że this nie jest NULL zatem jeśli obj jest NULL’em to wartości muszą być różne.
  2. Sprawdź czy wskaźniki pokazują ten sam obszar pamięci. Jeśli tak to mamy pewność, że są one sobie równe i nie musimy dokonywać zbędnych porównań.
  3. Jeśli obj.GetType() != this.GetType() zwróć również false – obiekty różnego typu w większości przypadków są różne (ale nie zawsze!).
  4. Porównaj wszystkie pola, właściwości obiektu this z obj. Jeśli, której z nich się różni zwróć od razu false.
  5. Wywołaj base. Equal aby wykonać analogiczne operacje ale dla bazowych właściwości (krok wymagany gdy mamy hierarchię klas).

Jak wspomniałem, object.Equal domyślnie porównuje referencję. Idąc tropem powyższych wskazówek, Equals powinien być zaimplementowany następująco (na przykładzie klasy Person zwierającej jedno pole FirstName):

class Person
{
   public string FirstName { get; set; }

   public override bool Equals(object other)
   {
       if (ReferenceEquals(null, other)) return false;
       if (ReferenceEquals(this, other)) return true;
       if (this.GetType() != other.GetType())
           return false;

       return Equals((Person)other);
   }
   public bool Equals(Person other)
   {
       return Equals(other.FirstName, FirstName);
   }
}

ReferenceEquals sprawdza czy wskaźniki są takie same. Oprócz Equals często wymagane jest zaimplementowanie metody GetHashCode() zwracającej hash obiektu. Wykorzystywany jest on np. w słownikach. Dużo szybsze jest porównanie liczby (hash’u) niż wszystkich pól po kolei. Z tego względu przed wywołaniem Equals, najpierw jest wywołane GetHashCode. Jeśli hash’e są różne wtedy mamy pewność, że obiekty są również różne i nie trzeba wywoływać już Equals. W przypadku jednak gdy hash’e są takie same, istnieje prawdopodobieństwo, że  obiekty mają takie same wartości. Wtedy dla pewności, należy wywołać Equals aby dokonać pełnego porównania. GetHashCode daje 100% pewność wyłącznie w sytuacjach gdy nie ma konfliktów – dwóch różnych obiektów o tym samym hash’u. Konfliktów nie możemy wykluczyć dlatego zawsze musimy wywoływać również Equals.

Przykładowa implementacja GetHashCode dla Person:

public override int GetHashCode()
{
  return (FirstName != null ? FirstName.GetHashCode() : 0);
}

GetHashCode musi być liczony za każdym razem od nowa – zdecydowanie odradzam optymalizacje polegające na buforowaniu hash’a np. w bazie danych. Implementacja .NET Framework’owa może się z czasem zmienić dlatego, zawsze jest konieczne porównanie “świeżo” wyliczonych wartości.

W c# istnieje jednak jeszcze jeden sposób porównania – za pomocą operatora ==.  Domyślna implementacja == po prostu porównuje wskaźniki tak jak ReferenceEquals.  Jeśli nie zaimplementowaliśmy operator== w naszej klasie wtedy poniższe wywołanie zwróci false:

Person person1 = new Person {FirstName = "Piotr"};
Person person2 = new Person { FirstName = "Piotr" };
Console.WriteLine(person1==person2); // FALSE - brak operator==
Console.WriteLine(object.Equals(person1, person2)); // TRUE

Dysponując metodą Equals, implementacja operator== oraz operator!= jest bardzo prosta:

class Person
{
   public string FirstName { get; set; }

   public static bool operator ==(Person left,
                                Person right)
   {
       return Equals(left,right);
   }

   public static bool operator !=(Person left, Person right)
   {
       return !(left == right);
   }
   public override bool Equals(object other)
   {
       if (ReferenceEquals(null, other)) return false;
       if (ReferenceEquals(this, other)) return true;
       if (this.GetType() != other.GetType())
           return false;

       return Equals((Person)other);
   }
   public bool Equals(Person other)
   {
       return Equals(other.FirstName, FirstName);
   }
   public override int GetHashCode()
   {
       return (FirstName != null ? FirstName.GetHashCode() : 0);
   }
}

Ważna uwaga: operatory w c# nie są wirtualne. Dlatego poniższe wywołanie może wydawać się dziwne:

object person1 = new Person {FirstName = "Piotr"};
object person2 = new Person { FirstName = "Piotr" };
Console.WriteLine(person1==person2); // FALSE !
Console.WriteLine(object.Equals(person1, person2));

Pierwsze porównanie zwróci false. Operatory są po prostu przeładowane (overloaded) a nie przedefiniowane (overriding). Oznacza to, że kompilator stara się dopasować jak najbardziej pasującą wersje metody operator==. Dla powyższego przypadku będzie to operator==(object,object) występujący w klasie object.  Rozważmy inny przykład:

class Person2:Person
{   
}

Person2 person1 = new Person2 { FirstName = "Piotr" };
Person2 person2 = new Person2 { FirstName = "Piotr" };
Console.WriteLine(person1==person2);// TRUE
Console.WriteLine(object.Equals(person1, person2));

Pomimo, że nie posługujemy się bezpośrednio klasą Person, kompilator uznał, że operator==(Person,Person) jest bardziej adekwatny niż operator==(object,object).  Zasada jest taka sama jak dla dwóch metodach o tej samej nazwie i różnych parametrach:

   static void Method(object a,object b)
   {       
     // 1
   }
   static void Method(Person a, Person b)
   {
    // 2
   }
   Method(new Person2(), new Person2()); // wywoła 2

 

Warto również zwrócić uwagę, że Equals  a operator== to dwie różne metody. Implementacja wyłącznie Equals nie daje możliwości wykorzystania porównania za pomocą zwykłego operatora ==.

Pisząc testy jednostkowe warto zwrócić uwagę na następujące przypadki:

  1. a.Equals( a ) zawsze musi zwracać true.
  2. a. Equals (NULL) zawsze musi zwracać false – chyba, że zaimplementowaliśmy NULL Pattern (special case pattern).
  3. Jeśli a.Equals( b ) zwraca true to b.Equals ( a ) również musi zwracać true.
  4. Jeśli a.Equals ( b) zwraca true oraz b.Equals ( c ) zwraca true to a. Equals ( c ) również musi zwracać true.

W .NET istnieją również interfejs IComparable zawierający metodę CompareTo. Wykorzystywana ona jest jedynie w sortowaniu a nie w porównywaniu dwóch wartości.

3 thoughts on “Porównywanie obiektów”

  1. Hej,

    Bardzo fajnie napisane, tylko prosiłbym o rozwinięcie:

    “3.Jeśli obj.GetType() != this.GetType() zwróć również false – obiekty różnego typu w większości przypadków są różne (ale nie zawsze!).”

    -Kiedy różnego typu obiekty mogą być takie same?
    -Jeżeli różnego typu obiekty nie zawsze są różne, to dlaczego zwracamy false bez dodatkowych sprawdzeń?

  2. Pawel:
    W niektorych bibliotekach dwie klasy w hierarchii moga byc roznego typu ale przechowywac te same dane. Po prostu klasa pochodna moze dodawac kilka dodatkowych metod. Stan zatem jest dokladnie ten sam. Wszystko zalezy oczywiscie od konkretnej sytuacji ale zdecydowanie nie jest to czesty przypdek.

  3. Post jest oczywiście technicznie (jak zwykle) poprawny, ale przyczepię się (jak zwykle) pewnej rzeczy – dotyczącej `najlepszych praktyk` i takich tam.

    Otóż, nie napisałeś że implementowania operator== nie wypada robić hurraoptymistycznie, bo zazwyczaj do osiągnięcia wymaganego celu służy Equals()

    1) O czym wspomniałeś – operatory nie są wirtualne, co może powodować trudne do zidentyfikowania problemy

    2) Dużo bardziej subtelna, ale nie mniej ważna rzecz – operator== to pewnego rodzaju kontrakt równoważności – intuicyjnie, daje pewność że jeśli a == b to a będzie równe b do momentu kiedy do jednego z nich nie będzie przypisana inna wartość (polecam sprawdzić, jest tak dla typów w bibliotece standardowej .NET).
    W przypadku kiedy pojawia się możliwość mutacji stanu, sprawy się robią tutaj skomplikowane…
    Żeby nie było że to moje fantazje – cytat z Guidelines for Overloading Equals() and Operator==:
    `When a type is immutable, meaning the data contained in the instance cannot be changed, overloading operator == to compare value equality instead of reference equality can be useful because, as immutable objects, they can be considered the same as long as they have the same value. Overriding operator == in non-immutable types is not recommended. `

    http://msdn.microsoft.com/en-us/library/ms173147%28v=vs.80%29.aspx
    http://stackoverflow.com/questions/4546720/overriding-the-operator-in-non-immutable-types

Leave a Reply

Your email address will not be published.