sobota, 13 października 2012

Zrozumieć Equality

Tworząc własny typ w większości przypadków definiujemy także dla niego funkcje równości.

Mało kto wie jednak że C# oferuje aż 4 takie funkcje:

Code:
public virtual bool Equals(object obj)
public static bool Equals(object objA, object objB)
public static bool ReferenceEquals(object objA, object objB)
public static bool operator ==(MyClass left, MyClass right)


Przeważnie nie definiujemy wszystkich 4 funkcji a tylko dwie z nich: Equals oraz operator ==
Należy pamiętać, że zmieniając jedną z nich możemy wpłynąć nieświadomie na pozostałe.
Oprócz tych 4 metod istnieje także interfes IEquatable<T>. Implementacja tego interfejsu jest wskazana tam, gdzie definiujemy funkcję Equals (interfejs ten zawiera jedną metodę Equals).
Mnogość metod sprawdzania równości wynika z prostego faktu:
  • Jeżeli porównujemy dwa obiekty, to są one równe gdy ich referencje wskazują na ten sam obiekt
  • Jeżeli porównujemy dwa typy wartościowe, to są one równe wtedy kiedy ich wartości są równe

Pierwsza z metoda, której nigdy nie zmieniamy - ReferenceEquals() zwraca prawdę, gdy dwie zmienne odnoszą się do tego samego obiektu - do tej samej komórki pamięci. Metoda ta nie sprawdza zawartości obiektu. Wynika z tego iż dla typów wartościowych zawsze zwraca fałsz (false). Nawet jeżeli porównany za pomocą tej metody zmienną wartościową z samą sobą dostaniemy false - związane jest to z boxing-iem.

Druga funkcja, której nigdy nie zmieniamy to statyczna funkcja Equals zdefiniowana w klasie Object:

Code:
public static bool Equals(object objA, object objB)
{
 return objA == objB || (objA != null && objB != null && objA.Equals(objB));
}


Funkcji tej używamy gdy nie znamy typu zmiennych, które porównujemy. Z kodu powyższego możemy odczytać, że rozważane są trzy przypadki:
  1. sprawdzenie za pomocą metody ReferenceEquals czy obiekty wskazują na tę samą komórkę pamięci
  2. sprawdzenie czy któryś z argumentów nie jest nullem
  3. wykorzystanie metody Equals


Kolejną metodą jest wirtualna metoda Equals. Kiedy ją nadpisujemy?
  • w przypadku typów wartościowych - zawsze
  • w przypadku typów referencyjnych - wtedy kiedy zależy nam na równości zawartości obiektu
Nadpisując metodę Equals warto zrobić to implementując interfejs IEquatable<T>
Zasady podczas nadpisywania metody Equals:
  • metoda ta nie powinna rzucać wyjątku - nie ma to większego sensu
  • w pierwszej kolejności sprawdzamy czy wchodzący do metody parametr nie jest nullem
  • drugie sprawdzenie dotyczy tego czy porównujemy dwa obiekty tego samego typu
  • implementując IEquatable<T>  - zapewniamy, że tworzymy silnie typowane sprawdzenie.
  • nadpisując metodę Equals należy także nadpisać GetHashCode
Przykładowa implementacja:

Code:
    public class Point : IEquatable<Point>
    {
        public int X { get; set; }
        public int Y { get; set; }

        public override bool Equals(object obj)
        {
            //1. sprawdzenie czy obiekt nie jest nullem
            if (obj == null)
            {
                return false;
            }

            //2. sprawdzenie czy porównujemy obiekty tego samego typu
            var point = obj as Point;
            if (point == null)
            {
                return false;
            }

            //3. porównanie zawartości obiektu
            return point.X == X && point.Y == Y;
        }

        public bool Equals(Point other)
        {
            if (other == null)
            {
                return false;
            }

            return other.X == X && other.Y == Y;
        }
    }



Nadpisanie operatora == jest rzeczą banalną. W przypadku typu wartościowego jego nadpisanie jest bardzo ważne w aspekcie wydajności - jeżeli tego nie zrobimy podczas porównywania będzie stosowana refleksja.
Jeżeli chodzi o typy referencyjne operator== sprawdza równość referencji. Jeżeli jest to tylko możliwe nie powinniśmy zmieniać tej funkcjonalności.

Brak komentarzy:

Prześlij komentarz