sobota, 13 października 2012

Pułapki podczas implementacji GetHashCode

Funkcja GetHashCode używana jest tylko i wyłącznie w przypadku korzystania z kolekcji typu klucz wartość jak HashSet<T> lub Dictionary<T1, T2>.
Funkcja ta nie jest tak oczywista jak się to zdaje i napisanie poprawnej jej wersji sprawia wiele problemów. Tworząc własny typ powinniśmy unikać jej implementacji, jednak jeżeli nasz typ zamierzamy użyć jako klucz słownika, jesteśmy zmuszeni do jej zdefiniowania. Kolekcje klucz - wartość wykorzystują rezultat funkcji GetHashCode w celu efektywnego wyszukiwania elementów w kolekcji.
Implementacja metody GetHashCode musi spełniać 3 warunki:
  1. jeżeli dwa obiekty są równe (operator==) - muszą generować ten sam hash
  2. niezależnie od zmienianych wartości w obiekcie, funkcja ta musi zwracać zawsze ten sam rezultat
  3. funkcja powinna generować losową wartość Integer dla danych wejściowych z całego zakresu tego typu danych.
Spełnienie 3 warunku jest szczególnie trudne. Domyślnie Object.GetHashCode() używa pola w którym przechowuje wartość hash. Każdy tworzony obiekt, w konstruktorze ma przypisany unikalną wartość typu Integer. Wartości te zaczynają się od 1 a następnie inkrementowane przy tworzeniu kolejnych wartości. Wartość ta jest przypisana w konstruktorze i nie może być później zmieniona. Funkcja GetHashCode() zwraca tę wartość.

W przypadku struktur funkcja ta zwraca hash dla pierwszego zadeklarowanego pola w niej. Przykładowo:

Code:
    public struct MyStruct
    {
        public string Name { get; set; }
        public int Id  { get; set; }
    }

dla tej struktury hash zostanie wygenerowany na podstawie pola Name.

Implementacja w przypadku klasy:

Code:
    public class Person
    {
        public string Name { get; private set; }
        public string Address { get; set; }

        public Person()
        {
        }

        public Person(string name)
        {
            Name = name;
        }

        public Person SetName(string name)
        {
            return new Person() { Name = name, Address = Address };
        }

        public override int GetHashCode()
        {
            return Name.GetHashCode();
        }
    }


Oraz użycie:

Code:
            var dictionary = new Dictionary<Person, string>();
            var p1 = new Person("Jan");
            dictionary.Add(p1, "20");
            Person p2 = p1.SetName("Marek");
            string s = dictionary[p1];
            dictionary.Remove(p1);
            dictionary.Add(p2, s);


Warto zauważyć tutaj dwie rzeczy:
  1. Właściwość Name jest tylko do odczytu - co zapewni nam że nie zostanie przez to zmieniony hash
  2. Mamy dodatkową metodę SetName, która zwraca nowy obiekt typu Person. W użyciu widać że w przypadku gdy zmienimy Name, należy stary obiekt wyrzucić ze słownika i zastąpić nowym. Rozwiązanie to umożliwia uniknięcie trudnych do wykrycia błędów związanych z tworzeniem hash-a.

Brak komentarzy:

Prześlij komentarz