Dlaczego ważne jest, aby nadpisać GetHashCode, gdy metoda Equals jest nadpisana?

Podano następującą klasę

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

Nadpisałem metodę Equals, Ponieważ Fooreprezentuje wiersz tabeli Foo s. Jaka jest preferowana metoda nadpisywania GetHashCode?

Dlaczego ważne jest obejście GetHashCode?

Author: Irshad, 2008-12-16

12 answers

Tak, ważne jest, czy twój element będzie używany jako klucz w słowniku, czy HashSet<T>, itd. - ponieważ jest on używany (w przypadku braku niestandardowego IEqualityComparer<T>) do grupowania elementów w wiadra. Jeśli hash-code dla dwóch elementów nie pasuje, mogą nigdy być uważane za równe (Equals po prostu nigdy nie zostanie wywołane).

Metoda GetHashCode() powinna odzwierciedlać logikę Equals; reguły są następujące:

  • Jeśli dwie rzeczy są równe (Equals(...) == true), to muszą zwrócić tę samą wartość dla GetHashCode()
  • Jeśli GetHashCode() jest równe, to nie jest konieczne, aby były takie same; jest to kolizja i Equals zostanie wywołana, aby sprawdzić, czy jest to prawdziwa równość, czy nie.

W tym przypadku wygląda na to, że" return FooId; " jest odpowiednią implementacją GetHashCode(). Jeśli testujesz wiele właściwości, często łączy się je za pomocą kodu jak poniżej, aby zmniejszyć kolizje diagonalne (tj. tak, że new Foo(3,5) ma inny kod hashowy do new Foo(5,3)):

int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;

Oh-for wygoda, można również rozważyć podanie operatorów == i != przy nadpisywaniu Equals i GetHashCode.


Pokaz tego, co się dzieje, gdy źle to robisz, jest tutaj .

 1127
Author: Marc Gravell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:29

Bardzo trudno jest poprawnie zaimplementować GetHashCode(), ponieważ oprócz wspomnianych już reguł, kod hashowy nie powinien się zmieniać podczas życia obiektu. Dlatego pola używane do obliczania kodu hash muszą być niezmienne.

W końcu znalazłem rozwiązanie tego problemu, gdy pracowałem z NHibernate. Moje podejście polega na obliczeniu kodu hash na podstawie ID obiektu. ID można ustawić tylko za pomocą konstruktora, więc jeśli chcesz zmienić ID, co jest bardzo mało prawdopodobne, musisz utworzyć nowy obiekt, który ma nowy ID, a tym samym nowy kod skrótu. To podejście działa najlepiej z GUID, ponieważ możesz dostarczyć parametryczny konstruktor, który losowo generuje identyfikator.

 123
Author: Albic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-11 18:55:11

Nadpisując równe, w zasadzie twierdzisz, że jesteś tym, który wie lepiej, jak porównać dwie instancje danego typu, więc prawdopodobnie będziesz najlepszym kandydatem do dostarczenia najlepszego kodu hashowego.

To jest przykład jak ReSharper pisze dla Ciebie funkcję GetHashCode ():
public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

Jak widać, po prostu próbuje odgadnąć dobry kod hashowy oparty na wszystkich polach w klasie, ale ponieważ znasz domenę obiektu lub zakresy wartości, nadal możesz zapewnić lepszą jeden.

 43
Author: Trap,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-02 10:21:35

Nie zapomnij sprawdzić parametru obj przed {[1] } podczas nadpisywania Equals(). A także porównać Typ.

public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

Powodem tego jest: Equals musi zwracać false w porównaniu do null. Zobacz też http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

 32
Author: huha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-03 20:14:13

A może:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

Zakładając, że wydajność nie jest problemem:)

 24
Author: Ludmil Tinkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-29 08:04:59

Jest tak dlatego, że framework wymaga, aby dwa obiekty, które są takie same, miały ten sam hashcode. Jeśli nadpisujesz metodę equals, aby wykonać specjalne porównanie dwóch obiektów i dwa obiekty są uważane za te same przez metodę, to kod skrótu obu obiektów musi być również taki sam. (Słowniki i Hashtable opierają się na tej zasadzie).

 9
Author: kemiller2002,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-16 20:36:40

Wystarczy dodać powyższe odpowiedzi:

Jeśli nie nadpisujesz równości, domyślnym zachowaniem jest porównywanie referencji obiektów. To samo dotyczy hashcode - Domyślna implementacja jest zazwyczaj oparta na adresie pamięci odniesienia. Ponieważ nadpisałeś równe oznacza to, że poprawnym zachowaniem jest porównywanie tego, co zaimplementowałeś na równych, a nie referencji, więc powinieneś zrobić to samo dla hashcode.

Klienci twojej klasy będą oczekiwać hashcode, aby mieć podobną logikę do metody equals, na przykład metody linq, które używają Ieequalitycomparer najpierw porównują hashcody i tylko jeśli są równe, porównają metodę Equals (), która może być droższa do uruchomienia, jeśli nie zaimplementowaliśmy hashcode, obiekt equal prawdopodobnie będzie miał różne hashcody (ponieważ mają inny adres pamięci) i zostanie błędnie określony jako nie równy (Equals () nawet nie trafi).

Ponadto, z wyjątkiem problemu, który może nie być w stanie znaleźć swój obiekt, jeśli użyłeś go w słowniku (ponieważ został wstawiony przez jeden hashcode i gdy go poszukasz, domyślny hashcode prawdopodobnie będzie inny i znowu Equals() nawet nie zostanie wywołane, jak wyjaśnia Marc Gravell w swojej odpowiedzi, wprowadzasz również naruszenie pojęcia słownika lub hashset, które nie powinno zezwalać na identyczne klucze - już zadeklarowałeś, że te obiekty są zasadniczo takie same, gdy przekroczysz równe, więc nie chcesz, aby oba z nich były różne klucze w strukturze danych, które mają unikalny klucz. Ale ponieważ mają inny hashcode," ten sam " klucz zostanie wstawiony jako inny.

 8
Author: BornToCode,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-07 10:24:28

Mamy dwa problemy do rozwiązania.

  1. Nie można podać sensownego GetHashCode(), jeśli jakieś pole w obiekt można zmienić. Również często obiekt nigdy nie będzie używany w zbiór zależny od GetHashCode(). Więc koszt implementacja GetHashCode() często nie jest tego warta, albo nie jest możliwe.

  2. Jeśli ktoś umieści Twój obiekt w kolekcji, która wywołuje GetHashCode() a Ty masz Equals() nie dokonując również GetHashCode() zachowywać się w prawidłowy sposób, że osoba może wydać dni namierzam problem.

Dlatego domyślnie tak.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}
 7
Author: Ian Ringrose,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-17 20:43:52

Hash code jest używany w kolekcjach opartych na hashach, takich jak Dictionary, Hashtable, HashSet itp. Celem tego kodu jest bardzo szybkie wstępne sortowanie określonego obiektu poprzez umieszczenie go w określonej grupie (bucket). To wstępne sortowanie ogromnie pomaga w znalezieniu tego obiektu, gdy trzeba go odzyskać z kolekcji hash, ponieważ kod musi szukać obiektu w jednym wiadrze zamiast we wszystkich obiektach, które zawiera. Im lepsza Dystrybucja kodów hashowych (lepsza unikalność), tym szybciej odzyskiwanie. W idealnej sytuacji, gdy każdy obiekt ma unikalny kod skrótu, znalezienie go jest operacją O(1). W większości przypadków zbliża się do O (1).

 5
Author: Maciej D.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-21 11:36:55

To niekoniecznie ważne; to zależy od wielkości twoich kolekcji i twoich wymagań wydajnościowych oraz od tego, czy Twoja klasa będzie używana w bibliotece, w której możesz nie znać wymagań wydajnościowych. Często wiem, że moje rozmiary kolekcji nie są zbyt duże, a mój czas jest cenniejszy niż kilka mikrosekund wydajności uzyskanych dzięki stworzeniu doskonałego kodu hashowego; więc (aby pozbyć się irytującego ostrzeżenia przez kompilator) po prostu używam: {]}

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }
(Oczywiście przydałoby mi się # pragma też wyłączyć ostrzeżenie, ale wolę w ten sposób.)

Kiedy jesteś w sytuacji, że robisz potrzebujesz wydajności, niż wszystkie kwestie wymienione przez innych tutaj mają zastosowanie, oczywiście. Najważniejsze - w przeciwnym razie otrzymasz błędne wyniki podczas pobierania elementów z zestawu skrótów lub słownika: kod skrótu nie może różnić się od czasu życia obiektu (dokładniej, w czasie, w którym kod skrótu jest potrzebny, np. gdy jest kluczem w słowniku): na przykład, poniższy kod jest błędny, ponieważ wartość jest publiczna i dlatego może być zmieniana zewnętrznie na klasę w czasie życia instancji, więc nie możesz jej używać jako podstawy dla kodu hashowego:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

Z drugiej strony, jeśli wartość nie może zostać zmieniona, można użyć:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }

 4
Author: ILoveFortran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-26 23:21:09

Rozumiem, że oryginalna funkcja GetHashCode () zwraca adres pamięci obiektu, więc konieczne jest nadpisanie jej, jeśli chcesz porównać dwa różne obiekty.

Edytowane: To było nieprawidłowe, oryginalna metoda GetHashCode() nie może zapewnić równości 2 wartości. Jednak obiekty, które są równe, zwracają ten sam kod skrótu.

 0
Author: user2855602,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-09 19:35:53

Poniżej użycie reflection wydaje mi się lepszą opcją biorąc pod uwagę właściwości publiczne, ponieważ w tym przypadku nie musisz się martwić o dodawanie / usuwanie właściwości (chociaż nie tak powszechny scenariusz). To okazało się być również lepsze.(Porównano czas za pomocą stopera Diagonalistycznego).

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }
 -5
Author: Guanxi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-25 18:49:18