2009-03-12 10 views
46

I klasy, który zawiera dwie następujące właściwości:GetHashCode ręczne obiektu zawierający ogólne tablicy

public int Id  { get; private set; } 
public T[] Values { get; private set; } 

dokonaniu to IEquatable<T> i nadpisane object.Equals tak:

public override bool Equals(object obj) 
{ 
    return Equals(obj as SimpleTableRow<T>); 
} 

public bool Equals(SimpleTableRow<T> other) 
{ 
    // Check for null 
    if(ReferenceEquals(other, null)) 
     return false; 

    // Check for same reference 
    if(ReferenceEquals(this, other)) 
     return true; 

    // Check for same Id and same Values 
    return Id == other.Id && Values.SequenceEqual(other.Values); 
} 

Kiedy o obejście object.Equals Oczywiście muszę również zastąpić GetHashCode. Ale jaki kod powinienem zaimplementować? Jak utworzyć hashcode z ogólnej tablicy? I jak połączyć go z liczbą całkowitą Id?

public override int GetHashCode() 
{ 
    return // What? 
} 

Odpowiedz

72

powodu problemów poruszonych w tym wątku, jestem delegowania kolejną odpowiedź pokazując, co się stanie, jeśli źle zrobisz ... głównie, że nie możesz użyć tablicy GetHashCode(); prawidłowe zachowanie polega na tym, że żadne ostrzeżenia nie są drukowane po uruchomieniu ...przełączyć komentarze to naprawić:

using System; 
using System.Collections.Generic; 
using System.Linq; 
static class Program 
{ 
    static void Main() 
    { 
     // first and second are logically equivalent 
     SimpleTableRow<int> first = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6), 
      second = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6); 

     if (first.Equals(second) && first.GetHashCode() != second.GetHashCode()) 
     { // proven Equals, but GetHashCode() disagrees 
      Console.WriteLine("We have a problem"); 
     } 
     HashSet<SimpleTableRow<int>> set = new HashSet<SimpleTableRow<int>>(); 
     set.Add(first); 
     set.Add(second); 
     // which confuses anything that uses hash algorithms 
     if (set.Count != 1) Console.WriteLine("Yup, very bad indeed"); 
    } 
} 
class SimpleTableRow<T> : IEquatable<SimpleTableRow<T>> 
{ 

    public SimpleTableRow(int id, params T[] values) { 
     this.Id = id; 
     this.Values = values; 
    } 
    public int Id { get; private set; } 
    public T[] Values { get; private set; } 

    public override int GetHashCode() // wrong 
    { 
     return Id.GetHashCode()^Values.GetHashCode(); 
    } 
    /* 
    public override int GetHashCode() // right 
    { 
     int hash = Id; 
     if (Values != null) 
     { 
      hash = (hash * 17) + Values.Length; 
      foreach (T t in Values) 
      { 
       hash *= 17; 
       if (t != null) hash = hash + t.GetHashCode(); 
      } 
     } 
     return hash; 
    } 
    */ 
    public override bool Equals(object obj) 
    { 
     return Equals(obj as SimpleTableRow<T>); 
    } 
    public bool Equals(SimpleTableRow<T> other) 
    { 
     // Check for null 
     if (ReferenceEquals(other, null)) 
      return false; 

     // Check for same reference 
     if (ReferenceEquals(this, other)) 
      return true; 

     // Check for same Id and same Values 
     return Id == other.Id && Values.SequenceEqual(other.Values); 
    } 
} 
+1

Czy możesz wyjaśnić rozumowanie właściwej wersji GetHashCode()? –

+4

@ Vinko: Czy możesz wyjaśnić? Masz na myśli "dlaczego kod hash ma znaczenie?" - lub "dlaczego to podejście?". Biorąc pod uwagę twoją reputację i liczbę odpowiedzi, zakładam to drugie; jest to po prostu sposób na uzyskanie wartości mieszającej, która uwzględnia wszystkie wartości pod uwagę "pomnóż przez liczbę pierwszą i dodaj następny skrót" to bardzo powszechne podejście do mieszania, które unika kolizji (kontrast xor; w takim przypadku zbiór "wszystkich 8s "może łatwo dać przewidywalny kod skrótu równy 0). Czy coś ominąłem? –

+0

Zobacz też: http: //stackoverflow.com/questions/263400#263416 ... inna liczba pierwsza, ale ten sam efekt. –

1
public override int GetHashCode() { 
    return Id.GetHashCode()^Values.GetHashCode(); 
} 

Istnieje kilka dobrych punktów w komentarzach i innych odpowiedzi. PO powinien rozważyć, czy wartości będą używane jako część "klucza", jeśli obiekt był używany jako klucz w słowniku. Jeśli tak, to powinny być częścią kodu skrótu, w przeciwnym razie nie.

Z drugiej strony, nie jestem pewien, dlaczego metoda GetHashCode powinna odzwierciedlać SequenceEqual. Ma na celu obliczenie indeksu w tabeli mieszania, a nie całkowite wyznaczenie równości. Jeśli istnieje wiele kolizji tabel mieszania przy użyciu powyższego algorytmu i jeśli różnią się one sekwencją wartości, należy wybrać algorytm uwzględniający sekwencję. Jeśli kolejność nie ma znaczenia, oszczędzaj czas i nie bierz tego pod uwagę.

+0

Ja też nie sądzę, tablice mają GetHashCode realizowane z uwzględnieniem wszystkich elementów – Grzenio

+0

które zrobi porównanie odniesienia na wartościach, a nie będzie kompatybilny z SequenceEqual (czyli dla różnych tablic z tych samych treści) –

+0

Chłopaki, "Już powiedziałem to wcześniej, ale * uważaj * używając wszystkich elementów widocznej tablicy. Wynik GetHashCode() powinien być taki sam przez cały okres istnienia obiektu, w przeciwnym razie nie będzie działał jako klucz hashtable. Nie ma gwarancji, że ta tablica się nie zmieni, więc nie używaj jej w GetHashCode! –

0

chciałbym zrobić to w ten sposób:

long result = Id.GetHashCode(); 
foreach(T val in Values) 
    result ^= val.GetHashCode(); 
return result; 
+1

całkiem rozsądne - zauważ, że xor może prowadzić do wielu kolizji; * ogólnie * preferowany jest mnożnik/dodatek –

+0

ciekawe, wiele osób powiedziało mi, że zamiast tego użyję xora. Powinienem przeczytać więcej na ten temat. – Grzenio

+2

W odpowiedzi na to; jaki byłby skrót (3,3,3,3)? i {4,4,4,4}? lub {4,0,0,4}? lub {1,0,1,0}? Widzisz problem ... –

0

dostarczonego że Id i wartości nigdy nie zmieni, a wartości nie jest zerowa ...

public override int GetHashCode() 
{ 
    return Id^Values.GetHashCode(); 
} 

pamiętać, że klasa nie jest niezmienna, ponieważ każdy może modyfikować zawartość wartości, ponieważ jest to tablica. Biorąc to pod uwagę, nie starałbym się generować kodu hashcode za pomocą jego zawartości.

+0

Spowoduje to porównanie z wartościami i nie będzie kompatybilne z SequenceEqual (tj. Dla różnych tablic o tej samej zawartości) –

+0

Dobrze, ale ponieważ tablica jest odsłonięta, a każdy zewnętrzny kod może ją zmienić, jest to naprawdę niebezpieczne. zawartości. –

+0

Więc powinienem po prostu użyć HashCode z Id? – Svish

2

Jak o czymś takim:

public override int GetHashCode() 
    { 
     int hash = Id; 
     if (Values != null) 
     { 
      hash = (hash * 17) + Values.Length; 
      foreach (T t in Values) 
      { 
       hash *= 17; 
       if (t != null) hash = hash + t.GetHashCode(); 
      } 
     } 
     return hash; 
    } 

ten powinien być zgodny z SequenceEqual, zamiast robić porównania odniesienia na tablicy.

+0

Niebezpiecznie jest porównywać zawartość wartości, ponieważ nie ma gwarancji, że będą takie same przez cały okres istnienia obiektu. Ponieważ tablica jest odsłonięta, każda zewnętrzna klasa może ją zmienić, co wpływa na hashcode! –

+0

Chodzi jednak o to, że jest on zgodny z metodą Equals w postaci opublikowanej. –

+0

Wpływa również na równość. I nie można użyć odniesienia do arary, aby obliczyć kod skrótu, ponieważ kończy się to dwoma równymi obiektami z różnymi kodami skrótu. – Grzenio

25

FWIW, bardzo niebezpieczne jest używanie zawartości wartości w haszowaniu. Powinieneś to zrobić tylko wtedy, gdy możesz zagwarantować, że nigdy się nie zmieni. Jednakże, ponieważ jest narażony, nie sądzę, że zagwarantowanie tego jest możliwe. Kod skrótu obiektu nigdy nie powinien się zmieniać. W przeciwnym razie traci swoją wartość jako klucz w Hashtable lub Dictionary. Weź pod uwagę trudny do znalezienia błąd użycia obiektu jako klucza w HashTable, jego zmiany hashcode ze względu na wpływ zewnętrzny i nie możesz go już znaleźć w HashTable!

+1

To wymaga więcej czasu. Zawsze miałem błędne założenie między pojęciem GetHashCode a "hash MD5" pobranego pliku. GetHashCode nie ma na celu porównywania zawartości, ale kontenera. Aby upewnić się, że wskazuje to samo miejsce w pamięci. Użyłem GetHashCode do sprawdzenia, czy obiekt zmienił się od czasu ostatniego zapisania go w bazie danych. Zachowałem sklonowaną listę tylko po to, aby porównać obiekty, ale po nadpisaniu GetHashCode wszystko na podstawie hashtable zaczęło zachowywać się dziwnie.Teraz przeniosłem moje przesłonięcie na własną metodę i utrzymuję słownik z "Zawieszeniem zawartości" – Pluc

+0

@Pluc: "GetHashCode ma na celu upewnienie się, że pojemnik wskazuje to samo miejsce w pamięci.", Niezupełnie. To * ma * na celu porównanie zawartości, po prostu może mieć fałszywe alarmy w wyniku kolizji. Podobnie jak MD5, ale z większą szansą na kolizje. – Groo

3

Ponieważ hashCode jest trochę kluczem do przechowywania przedmiotów (lllike w hashtable), chciałbym użyć tylko Id.GetHashCode()

+0

Cóż, jest to lepsze niż użycie Values.GetHashCode(), ponieważ zachowuje zgodność z Equals. –

0

Znam ten wątek jest dość stary, ale napisałem tę metodę, aby umożliwić mi obliczyć hashcodes z wielu obiektów. To bardzo pomocne w tym przypadku. Nie jest doskonały, ale spełnia moje potrzeby i najprawdopodobniej również twoje.

Nie mogę tego naprawdę wziąć za zasługę. Dostałem tę koncepcję z niektórych implementacji .net gethashcode. Używam 419 (ostatecznie, to mój ulubiony duży prime), ale możesz wybrać prawie każdą rozsądną liczbę pierwszą (nie za małą ... nie za dużą).

Więc, oto jak dostanę moje hashcodes:

using System.Collections.Generic; 
using System.Linq; 

public static class HashCodeCalculator 
{ 
    public static int CalculateHashCode(params object[] args) 
    { 
     return args.CalculateHashCode(); 
    } 

    public static int CalculateHashCode(this IEnumerable<object> args) 
    { 
     if (args == null) 
      return new object().GetHashCode(); 

     unchecked 
     { 
      return args.Aggregate(0, (current, next) => (current*419)^(next ?? new object()).GetHashCode()); 
     } 
    } 
} 
-1

po prostu musiałem dodać kolejną odpowiedź, ponieważ jeden z bardziej oczywiste (i najłatwiejszym do wdrożenia) rozwiązania nie zostały wymienione - nie licząc zbiorów w twojej GetHashCode obliczenie!

Najważniejszą rzeczą, o której zapomniałem, jest to, że niepowtarzalność wyniku GetHashCode nie jest wymagana (lub w wielu przypadkach nawet jest to możliwe). Nierówne obiekty nie muszą zwracać nierównych kodów skrótu, jedynym wymaganiem jest to, że równe obiekty zwracają równe kody skrótu. Więc o tej definicji, co następuje realizacja GetHashCode jest poprawna dla wszystkich obiektów (zakładając, że jest to poprawne Equals realizacja):

public override int GetHashCode() 
{ 
    return 42; 
} 

Oczywiście byłoby to wydajność najgorszą możliwą wydajność w hashtable odnośnika, O (n) zamiast O (1), ale nadal jest funkcjonalnie poprawny. Z tego względu moją ogólną rekomendacją podczas implementacji GetHashCode dla obiektu, który ma kolekcję dowolnego typu jako jednego lub więcej jej członków, jest po prostu zignorowanie ich i obliczenie wartości GetHashCode wyłącznie na podstawie pozostałych elementów skalarnych. To mogłoby działać całkiem dobrze, z wyjątkiem sytuacji, gdy wstawisz do tabeli mieszania ogromną liczbę obiektów, w których wszystkie ich elementy skalarne mają identyczne wartości, co daje identyczne kody skrótu.

Ignorowanie elementów kolekcji podczas obliczania kodu skrótu może również poprawić wydajność, pomimo zmniejszenia rozkładu wartości kodu skrótu. Pamiętaj, że użycie kodu skrótu ma poprawić wydajność w tabeli mieszania, nie wymagając połączenia z numerem Equals N, a zamiast tego będzie wymagać tylko raz wywołania GetHashCode i szybkiego sprawdzania tablicy hash. Jeśli każdy obiekt ma wewnętrzną tablicę zawierającą 10 000 elementów, które wszystkie uczestniczą w obliczaniu kodu skrótu, wszelkie korzyści uzyskane przez dobrą dystrybucję zostaną prawdopodobnie utracone. Byłoby lepiej mieć nieznacznie mniej rozproszonego kodu skrótu, jeśli generowanie go jest znacznie mniej kosztowne.

+0

Celem kodu skrótu jest nie tylko wybranie łyżki mieszającej , ale ogólniej, aby szybko pozbyć się rzeczy, które można uznać za nierówne. Klasa powinna opierać swoją koncepcję równości na tej z enkapsulowanej sekwencji, jeśli sekwencja jest niezmienna. Zakładając, że sekwencja jest niezmienna, klasa powinna prawdopodobnie zawierać elementy sekwencji w jej obliczonym haszymy (który z kolei powinien prawdopodobnie buforować). W przeciwnym razie, jeśli doda się do słownika dziesięć obiektów z macierzami o 5000 pozycji, które różnią się w ostatnim elemencie, próba znalezienia elementu spowoduje ... – supercat

+0

... wszystkie 5000 elementów nowego elementu zostanie porównanych do wszystkich 5000 elementów każdy z dziesięciu obiektów. W przeciwieństwie do tego, jeśli każdy element obliczył i buforował wartość skrótu dla zawartości tablicy, nawet jeśli wszystkie dziesięć wartości skrótu zostało zmapowane do tego samego bloku mieszania, najbardziej, co by się stało, gdyby wszystkie wartości mieszania były różne, byłaby to wartość mieszająca nowy obiekt byłby porównywany z buforowanymi wartościami mieszającymi pozostałych dziesięciu. Jeśli kilka wartości mieszających się zderzy, to nadal nie będzie to prawdziwy problem - tylko jedna dodatkowa porcja porównań 5000 elementów (zamiast dziesięciu). – supercat

+0

@supercat: Robisz tutaj wiele założeń: że sekwencja jest niezmienna, że ​​obiekt przechowuje swój własny kod skrótu (nigdy tego nie widziałem), ale co najważniejsze, to tylko dane obiektu, na którym bazuje kod skrótu jest sekwencją (zauważ, że w pytaniu oryginalnym obiekt ma właściwość "Id", która w prawie wszystkich przypadkach wystarcza do wygenerowania unikalnego kodu hash). W każdym razie, mówisz o bardzo szczególnym scenariuszu, którego nie rozumiem, w jaki sposób odnosi się do ogólnego przypadku lub pierwotnego pytania. –