GetHashCode() - czym dokładnie jest?

0

Nie rozumiem czym dokładnie jest wynik zwracany przez GetHashCode(), mogły ktoś jeszcze swoimi słowami podpowiedzieć czym to jest?

Musiałem ponadpisywać metody porównania i potworzyć przeciążenia operatorów ==,!=,+,- dla swojego typu, o ile bez problemu rozumiem na czym mają polegać metody Equal() i przeciążenia operatorów, kompletnie nie rozumiem co się dzieje w GetHashCode() - tą implementacje zerżnąłem z książki.

class CzasScala
{
///.....
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            if (this.GetType() != obj.GetType())
                return false;
            return Equals((CzasScala)obj);
        }
        private bool Equals(CzasScala obj)
        {
            if (ReferenceEquals(this, obj))
                return true;
            if (this.GetHashCode() != obj.GetHashCode())
                return false;

            return (this.SumaSekund == obj.sumaSekund) &&
                (this.dokladnosc_CzasDec_PoPrzecinku == obj.dokladnosc_CzasDec_PoPrzecinku);
        }
        public override int GetHashCode()
        {
            int hashCode = dokladnosc_CzasDec_PoPrzecinku.GetHashCode();
            if (dokladnosc_CzasDec_PoPrzecinku.GetHashCode() != SumaSekund.GetHashCode())
            {
                hashCode ^= SumaSekund.GetHashCode();
            }

            return hashCode;
        }
}

alternatywa po odwróceniu wlasciwosci /dokladnosc_czasDec_PoPrzecinku - używam do zaprezentowania czas w formie wartości dziesiętnej, to pole jest prywatne i ma zawsze wartosc 4:

        public override int GetHashCode()
        {
            int hashCode = SumaSekund.GetHashCode();
            if (dokladnosc_CzasDec_PoPrzecinku.GetHashCode() != SumaSekund.GetHashCode())
            {
                hashCode ^= dokladnosc_CzasDec_PoPrzecinku.GetHashCode();
            }
            return hashCode;
        }
}

Najbardziej nie rozumiem wymogu dla GetHashCode() aby

"wartości GetHashCode() w czasie życia danego obiektu powinny być takie same - także wtedy gdy dane w obiekcie się zmieniają - "C# kompletny przewodnik dla praktyków" Mark Michaelis str.378"

ale po zerżnięciu implementacji to wymaganie nie jest spełnione i widać to w wynikach z konsoli w ostatniej linii, rozumiem dlaczego bo przecież SumaSekund się zmieniła z 67 na 65 ale nie rozumiem jak to zrobić aby po zmianie SumaSekund wartość HashCode się nie zmieniła.

            CzasScala czas1 = new CzasScala(67);
            CzasScala czas2 = new CzasScala(2);
            CzasScala czas3 = new CzasScala(65);
            CzasScala czas4 = new CzasScala(65);
            CzasScala czas5 = czas3;
            CzasScala czas6 = new CzasScala(4);

            Console.WriteLine($"Czas: {czas1.SumaSekund}, HashCode czas1: {czas1.GetHashCode()}");
            Console.WriteLine($"Czas: {czas2.SumaSekund}, HashCode czas2: {czas2.GetHashCode()}");
            Console.WriteLine($"Czas: {(czas1 - czas3).SumaSekund}, HashCode czas1-czas3: {(czas1 - czas3).GetHashCode()}");
            Console.WriteLine($"Czas: {czas3.SumaSekund}, HashCode czas3: {czas3.GetHashCode()}");
            Console.WriteLine($"Czas: {czas4.SumaSekund}, HashCode czas4: {czas4.GetHashCode()}");
            Console.WriteLine($"Czas: {czas5.SumaSekund}, HashCode czas5: {czas5.GetHashCode()}");
            Console.WriteLine($"Czas: {czas6.SumaSekund}, HashCode czas6: {czas6.GetHashCode()}");
            Console.WriteLine($"Czas: {(czas2+czas2).SumaSekund}, HashCode (czas2+czas2): {(czas2 + czas2).GetHashCode()}");
            czas1 = (czas1 - czas2);
            Console.WriteLine($"Czas: {czas1.SumaSekund}, HashCode czas1 po zmianie wartosci o -czas2 czy hashcode bedzie rowny czas3?: {czas1.GetHashCode()}");

            Console.ReadLine();

Zdjęcie strony 378
IMG_20210413_090917.jpg

3

Cyba warto rzucić okiem na podstawy teoretyczne:
https://pl.wikipedia.org/wiki/Funkcja_skr%C3%B3tu
-- jak to Ci nie rozjaśni, to pytaj dalej (ale po przeczytaniu artykułu z Wikipedii! :))

1

A na marginesie: nie wydaje Wam się, że Wymagane nr 1 jest w sprzeczności z Wymaganym nr 2 (ze zdjęcia, które @Varran opublikował) -- jeśli obiekt jest mutowalny? Jak z tego wybrnąć?

PS. Poza nieużywaniem obiektów mutowalnych (popieram)... :)

4

@Varran: no nie o to mi chodziło, więc spróbuję dokładniej w poście, bo w komentarzu i tak się nie zmieści.

Encja to coś, co ma swoją tożsamość, np. człowiek, faktura - człowieka identyfikujesz po peselu, fakturę po numerze, w ogólności dowolną encję możesz po jakimś jej naturalnym identyfikatorze odróżnić. Encja w ogólności nie ma żadnego związku z bazą danych.
Value object to coś, co nie ma własnej tożsamości, więc żeby porównać dwa takie obiekty trzeba porównać ich zawartość. Value objecty to np. punkt na płaszczyźnie (jeśli współrzędne są równe, to to jest ten sam punkt), cena (jeśli kwota i waluta się zgadzają to to jest taka sama cena), itp.

Przy tym założeniu, to w przypadku encji GetHashCode to będzie GetHashCode jej identyfikatora. W przypadku value objectów GetHashCode musi używać wszystkich pól, aby mogło się zmienić po zmianie wartości któregokolwiek pola (czyli wtedy, gdy zmieni się też wynik Equals).

Nie wiem co dokładnie Twój kod ma robić, ale na pewno możesz swoje obiekty przypisać do jednej z tych dwóch grup i odpowiednio zaimplementować GetHashCode. I wtedy będzie to działanie zgodne z tym, co jest opisane w książce.

A czyste encje z bazy sa i tak czyste bez zadnych metod bo sluza tylko do potworzenia obiektow w logice?

Ja nie nazywam takich obiektów encjami tylko persistence modelami, właśnie żeby uniknąć zamieszania z nazewnictwem. Te persistence modele zazwyczaj są encjami (bo maja swoją tożsamość definiowaną przez jakiś unikalny identyfikator, w najgorszym razie nawet sztuczny klucz z bazy). Ale w ogóle nie ma znaczenia, czy masz takie anemiczne modele, czy mapujesz tabele do jakichś pełnoprawnych "bogatych" encji, liczy się to jak je identyfikujesz.

5

Wydaje mi się że nikt nie odpowiedział na zadane pytanie ani z czego wynikają te wymagania.
GetHashCode ma po prostu zwrócić liczbę która wstępnie zaklasyfikuje obiekt. Używane to jest przykładowo w słownikach, gdzie dodane dane są podzielone na "kubełki" (buckets). Dla uproszczenia powiedzmy że obiekt trafia do jednego z 10 kubełków na podstawie ostatniej cyfry. Gdy próbujesz potem sprawdzić czy wartość jest w słowniku, najpierw bierzemy GetHashCode obiektu który sprawdzasz i przeglądamy zawartość tylko jednego z tych kubełków porównując je po kolei (używając Equals).

Jak pewnie się domyślasz - pierwsze dwa wymagania wynikają z tego że musisz szukać obiektu w tym samym kubełku do którego je wrzuciłeś.
Mógłbyś zawsze zwracać stałą liczbę, powiedzmy "0" - wtedy wszystkie obiekty trafią do tego samego kubła i na pewno je odnajdziesz z powrotem - ale potem przy sprawdzaniu będziesz musiał przejrzeć wszystkie obiekty wrzucone do jednego kubełka zamiast tylko 1/10 co będzie miało wpływ na wydajność (ostatnie wymaganie).

Tak naprawdę zasady są luźniejsze - dotyczą tylko elementów których używasz właśnie w hashowanych kolekcjach jako klucz. Nie powinno się tam używać obiektów mutowalnych (zazwyczaj kluczem i tak jest typ prosty jak liczba / GUID czy string) dlatego też możesz spokojnie generować hashcode na podstawie danych. Hashcode może też się zmieniać dowoli dopóki obiekt nie zostanie do takiej kolekcji dodany, czasami stosuje się "zamrażanie" obiektów https://github.com/fodyarchived/Freezable - do pewnego momentu obiekt jest mutowalny, w pewnym momencie następuje jego zamrożenie i dalsze zmiany nie są możliwe - wtedy może nastąpić wyliczenie hashcode'u.
W praktyce praktycznie nie widziałem innej implementacji niż wyliczania hashcode'u na podstawie danych. Gdy zrozumiesz jak to wszystko działa będzie Ci łatwiej podjąć decyzję.

Co ciekawe - z rozmów kwalifikacyjnych wiem że ponad 90% programistów w ogóle nie rozumie czym jest HashCode. Na pytanie "co się stanie gdy spróbujemy dodać do Dictionary drugi obiekt o tym samym hashcodzie" w rozmowie na stanowisko seniorskie(!) znaczna większość odpowiada że zostanie rzucony wyjątek lub że element nie zostanie dodany do kolekcji.

0

zamiast klepać z ręki hashcody, to użyj VS i on ci wygeneruje ładny GetHashCode, a w dodatku zastosuje HashCode.Combine

screenshot-20210413185313.png

screenshot-20210413185343.png+

@obscurity

Co ciekawe - z rozmów kwalifikacyjnych wiem że ponad 90% programistów w ogóle nie rozumie czym jest HashCode. Na pytanie "co się stanie gdy spróbujemy dodać do Dictionary drugi obiekt o tym samym hashcodzie" w rozmowie na stanowisko seniorskie(!) znaczna większość odpowiada że zostanie rzucony wyjątek lub że element nie zostanie dodany do kolekcji.

jesteś tego pewien co tutaj napisałeś? ;)

bo wiesz, głupio byłoby gdybyś się tu chichrał z señor, a mimo to pisał coś, co nie odzwierciedla rzeczywistości :P

1

@Varran: https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0
W dokumentacji Microsoftu napisane jest, że tych nieszczęsnych kodów skrótów powinno się używać wyłącznie w kolekcjach takich jak Dictionary<TKey,TValue> ,Hashtable lub pochodnych klasy DictionaryBase

1 użytkowników online, w tym zalogowanych: 0, gości: 1