Apa algoritma terbaik untuk mengganti GetHashCode?

1449

Di .NET, GetHashCodemetode ini digunakan di banyak tempat di seluruh pustaka kelas dasar .NET. Menerapkannya dengan benar sangat penting untuk menemukan item dengan cepat dalam koleksi atau ketika menentukan kesetaraan.

Apakah ada algoritma standar atau praktik terbaik tentang cara menerapkan GetHashCodeuntuk kelas khusus saya sehingga saya tidak menurunkan kinerja?

bitbonk
sumber
38
Setelah membaca pertanyaan ini dan artikel di bawah ini, saya bisa menerapkan override GetHashCode. Saya berharap ini akan bermanfaat bagi orang lain. Pedoman dan aturan untuk GetHashCode ditulis oleh Eric Lippert
rene
4
"atau untuk menentukan kesetaraan": tidak! Dua objek dengan kode hash yang sama belum tentu sama.
Thomas Levesque
1
@ThomasLevesque Anda benar, dua objek dengan kode hash yang sama belum tentu sama. Tetapi masih GetHashCode()digunakan dalam implementasi yang sangat banyak Equals(). Itulah yang saya maksud dengan pernyataan itu. GetHashCode()di dalam Equals()sering digunakan sebagai jalan pintas untuk menentukan ketidaksetaraan , karena jika dua objek memiliki kode hash yang berbeda mereka harus menjadi objek yang tidak sama dan sisanya dari pemeriksaan kesetaraan tidak harus dieksekusi.
bitbonk
3
@ Bitbonk Biasanya, keduanya GetHashCode()dan Equals()perlu melihat semua bidang kedua objek (Persamaan harus melakukan ini jika kode hash sama atau tidak-dicentang). Karena itu, panggilan ke GetHashCode()dalam Equals()seringkali berlebihan dan dapat mengurangi kinerja. Equals()mungkin juga dapat melakukan hubungan pendek, membuatnya lebih cepat - namun dalam beberapa kasus kode hash mungkin di-cache, membuat GetHashCode()pemeriksaan lebih cepat dan sangat bermanfaat. Lihat pertanyaan ini untuk lebih lanjut.
NotEnoughData
UPDATE JAN 2020: Blog Eric Lippert berlokasi di: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

Jawaban:

1604

Saya biasanya pergi dengan sesuatu seperti implementasi yang diberikan di Jawa Efektif luar biasa Josh Bloch . Ini cepat dan menciptakan hash yang cukup bagus yang tidak mungkin menyebabkan tabrakan. Pilih dua bilangan prima yang berbeda, mis. 17 dan 23, dan lakukan:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

Seperti disebutkan dalam komentar, Anda mungkin lebih baik memilih bilangan prima besar untuk dikalikan dengan sebagai gantinya. Tampaknya 486187739 baik ... dan meskipun sebagian besar contoh yang saya lihat dengan angka kecil cenderung menggunakan bilangan prima, setidaknya ada algoritma yang sama di mana angka non-prime sering digunakan. Dalam contoh yang tidak cukup- FNV nanti, misalnya, saya telah menggunakan angka yang tampaknya bekerja dengan baik - tetapi nilai awal bukan yang utama. (Konstanta pengali adalah prima. Saya tidak tahu seberapa penting itu.)

Ini lebih baik daripada praktik umum XORkode hash karena dua alasan utama. Misalkan kita memiliki tipe dengan dua intbidang:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

Omong-omong, algoritma sebelumnya adalah yang saat ini digunakan oleh kompiler C # untuk tipe anonim.

Halaman ini memberikan beberapa opsi. Saya pikir untuk sebagian besar kasus di atas adalah "cukup baik" dan sangat mudah untuk diingat dan diperbaiki. The FNV alternatif adalah sama sederhana, tetapi menggunakan konstanta yang berbeda dan XORbukan ADDsebagai operasi menggabungkan. Ini terlihat sesuatu seperti kode di bawah, tetapi algoritma FNV yang normal beroperasi pada byte individu, jadi ini akan membutuhkan memodifikasi untuk melakukan satu iterasi per byte, bukan per nilai hash 32-bit. FNV juga dirancang untuk panjang data variabel, sedangkan cara kita menggunakannya di sini selalu untuk jumlah nilai bidang yang sama. Komentar pada jawaban ini menunjukkan bahwa kode di sini tidak benar-benar berfungsi dengan baik (dalam contoh kasus diuji) sebagai pendekatan tambahan di atas.

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

Perhatikan bahwa satu hal yang perlu diperhatikan adalah bahwa idealnya Anda harus mencegah negara Anda yang sensitif terhadap kesetaraan (dan dengan demikian memiliki kode hash) berubah setelah menambahkannya ke koleksi yang bergantung pada kode hash.

Sesuai dokumentasi :

Anda dapat mengganti GetHashCode untuk tipe referensi yang tidak dapat diubah. Secara umum, untuk tipe referensi yang bisa berubah, Anda harus mengganti GetHashCode hanya jika:

  • Anda dapat menghitung kode hash dari bidang yang tidak bisa diubah; atau
  • Anda dapat memastikan bahwa kode hash dari objek yang tidak bisa diubah tidak berubah saat objek tersebut terkandung dalam koleksi yang bergantung pada kode hash-nya.
Jon Skeet
sumber
8
Algoritme yang dijelaskan dalam buku yang Anda sebutkan sebenarnya sedikit lebih rinci, terutama menjelaskan apa yang harus dilakukan untuk berbagai tipe data bidang. Misalnya: untuk bidang tipe long use (int) (bidang ^ f >>> 32) alih-alih hanya memanggil GetHashcode. Apakah long.GetHashCodes diimplementasikan seperti itu?
bitbonk
13
Yup, Int64.GetHashCode melakukan hal itu. Di Jawa itu tentu membutuhkan tinju. Itu mengingatkan saya - waktu untuk menambahkan tautan ke buku ...
Jon Skeet
77
23 bukanlah pilihan yang baik, karena (pada .net 3.5 SP1) Dictionary<TKey,TValue>mengasumsikan modulo distribusi prima baik. Dan 23 adalah salah satunya. Jadi jika Anda memiliki kamus dengan Kapasitas 23 hanya kontribusi terakhir untuk GetHashCodememengaruhi kode hash gabungan. Jadi saya lebih suka menggunakan 29 daripada 23.
CodesInChaos
23
@CodeInChaos: Hanya kontribusi terakhir yang memengaruhi bucket - jadi mungkin, paling buruk, harus memeriksa semua 23 entri dalam kamus. Masih akan memeriksa kode hash aktual dari setiap entri, yang akan murah. Jika Anda memiliki kamus sekecil itu, itu tidak terlalu berarti.
Jon Skeet
20
@ Vajda: Saya biasanya menggunakan 0 sebagai kode hash yang efektif untuk null- yang tidak sama dengan mengabaikan bidang.
Jon Skeet
431

Jenis Anonim

Microsoft sudah menyediakan generator HashCode generik yang baik: Cukup salin nilai properti / bidang Anda ke jenis anonim dan hash:

new { PropA, PropB, PropC, PropD }.GetHashCode();

Ini akan berfungsi untuk sejumlah properti. Itu tidak menggunakan tinju. Itu hanya menggunakan algoritma yang sudah diterapkan dalam kerangka kerja untuk jenis anonim.

ValueTuple - Pembaruan untuk C # 7

Seperti @cactuaroid menyebutkan dalam komentar, tuple nilai dapat digunakan. Ini menghemat beberapa penekanan tombol dan yang lebih penting dijalankan secara murni di stack (tidak ada Sampah):

(PropA, PropB, PropC, PropD).GetHashCode();

(Catatan: Teknik asli menggunakan jenis anonim tampaknya membuat objek pada heap, yaitu sampah, karena jenis anonim diimplementasikan sebagai kelas, meskipun ini mungkin dioptimalkan oleh kompiler. Akan menarik untuk membandingkan opsi ini, tetapi Opsi tuple harus lebih unggul.)

Rick Love
sumber
85
Ya, GetHashCodeimplementasi anonim sangat efektif (BTW itu sama dengan yang ada di jawaban Jon Skeet), tetapi satu-satunya masalah dengan solusi ini adalah Anda menghasilkan instance baru pada GetHashCodepanggilan apa pun . Ini bisa menjadi sedikit overhead-ish khususnya dalam hal akses intensif ke koleksi hash besar ...
digEmAll
5
@ DigEmAll Poin bagus, saya tidak memikirkan overhead untuk membuat objek baru. Jawaban Jon Skeet adalah yang paling efisien dan tidak akan menggunakan tinju. (@Kumba Untuk mengatasi yang tidak diperiksa di VB, cukup gunakan Int64 (panjang) dan potong setelah perhitungan.)
Rick Love
42
bisa new { PropA, PropB, PropC, PropD }.GetHashCode()juga mengatakan
1111
17
VB.NET harus menggunakan Key dalam pembuatan tipe anonim: New With {Key PropA}.GetHashCode()Jika tidak GetHashCode tidak akan mengembalikan kode hash yang sama untuk objek yang berbeda dengan properti 'pengidentifikasi' yang sama.
David Osborne
4
@Keith dalam hal ini, saya akan mempertimbangkan menyimpan IEnumerable sebagai nilai daftar di suatu tempat alih-alih menghitungnya setiap kali kode hash dihitung. Caclulating ToList setiap kali di dalam GetHashCode dapat merusak kinerja dalam banyak situasi.
Rick Love
105

Inilah pembantu kode hash saya.
Keuntungannya adalah ia menggunakan argumen tipe umum dan karenanya tidak akan menyebabkan tinju:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

Juga memiliki metode ekstensi untuk menyediakan antarmuka yang lancar, sehingga Anda dapat menggunakannya seperti ini:

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

atau seperti ini:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}
nightcoder
sumber
5
Tidak perlu secara T[]terpisah karena sudahIEnumerable<T>
nawfal
5
Anda bisa memperbaiki metode-metode itu dan membatasi logika inti ke satu fungsi
nawfal
12
Kebetulan, 31 adalah pergeseran dan pengurangan pada CPU, yang sangat cepat.
Chui Tey
4
@ nightcoder Anda bisa menggunakan params .
ANeves
6
@ ChuiTey Ini adalah kesamaan yang dimiliki semua Mersenne Primes .
Pharap
63

Saya memiliki kelas Hashing di perpustakaan Helper yang saya gunakan untuk tujuan ini.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

Kemudian, cukup Anda dapat menggunakannya sebagai:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

Saya tidak menilai kinerjanya, jadi ada umpan balik yang disambut.

Wahid Shalaly
sumber
26
Yah, itu akan menyebabkan tinju, jika bidang adalah tipe nilai.
nightcoder
5
"dapat ditingkatkan kemudian dengan menangkap OverflowException" Inti dari uncheckedadalah untuk menghindari pengecualian pada overflow yang diinginkan pada GetHashCode. Jadi tidak salah kalau nilainya melimpah intdan tidak ada salahnya sama sekali.
Tim Schmelter
1
Salah satu masalah dengan algoritma ini adalah bahwa setiap array yang penuh dengan nol akan selalu mengembalikan 0, terlepas dari panjangnya
Nathan Adams
2
Metode penolong ini juga mengalokasikan objek baru []
James Newton-King
1
Seperti yang disebutkan oleh @NathanAdams, fakta yang nulldilewati sepenuhnya dapat memberikan hasil yang tidak terduga. Alih-alih melewatkannya, Anda harus menggunakan beberapa nilai konstan alih-alih input[i].GetHashCode()ketika input[i]bernilai nol.
David Schwartz
58

Inilah kelas pembantu saya menggunakan implementasi Jon Skeet .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

Pemakaian:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

Jika Anda ingin menghindari menulis metode ekstensi untuk System.Int32:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

Itu masih menghindari alokasi tumpukan dan digunakan dengan cara yang persis sama:

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

Sunting (Mei 2018): EqualityComparer<T>.Defaultpengambil sekarang adalah intrinsik JIT - permintaan tarik disebutkan oleh Stephen Toub dalam posting blog ini .

Şafak Gür
sumber
1
Saya akan mengubah saluran dengan operator tersier menjadi:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry
Saya percaya bahwa operator ternary obj != nullakan mengkompilasi boxinstruksi yang akan mengalokasikan memori jika Tmerupakan tipe nilai. Sebagai gantinya Anda dapat menggunakan obj.Equals(null)yang akan dikompilasi ke panggilan virtual Equalsmetode ini.
Martin Liversage
Karena this.hashCode != h. Itu tidak akan mengembalikan nilai yang sama.
Şafak Gür
Maaf, kelola untuk menghapus komentar saya alih-alih mengeditnya. Apakah lebih bermanfaat untuk membuat struct baru kemudian mengubah kode hash menjadi non-readonly dan lakukan: "hapus centang {this.hashCode ^ = h * 397;} kembalikan ini;" sebagai contoh?
Erik Karlsson
Kekekalan memiliki manfaatnya ( Mengapa kejahatan yang bisa berubah itu jahat? ). Tentang kinerja, apa yang saya lakukan cukup murah karena tidak mengalokasikan ruang di tumpukan.
Şafak Gür
30

.NET Standard 2.1 Dan Di Atas

Jika Anda menggunakan .NET Standard 2.1 atau di atasnya, Anda dapat menggunakan struct System.HashCode . Ada dua metode untuk menggunakannya:

HashCode.Combine

The Combinemetode dapat digunakan untuk membuat kode hash, mengingat hingga delapan objek.

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

The Addmetode membantu Anda untuk berurusan dengan koleksi:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

GetHashCode Menjadi Mudah

Anda dapat membaca posting blog lengkap ' GetHashCode Made Easy ' untuk rincian dan komentar lebih lanjut.

Contoh Penggunaan

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

Penerapan

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) => 
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

Apa yang Membuat Algoritma Baik?

Kecepatan

Algoritma yang menghitung kode hash harus cepat. Algoritma sederhana biasanya akan menjadi yang lebih cepat.

Deterministik

Algoritma hashing perlu deterministik yaitu diberi input yang sama harus selalu menghasilkan output yang sama.

Kurangi Tabrakan

Algoritma yang menghitung kode hash perlu menjaga tabrakan hash ke minumum. Tabrakan hash adalah situasi yang terjadi ketika dua panggilan ke GetHashCodedua objek yang berbeda menghasilkan kode hash yang identik. Perhatikan bahwa tabrakan diperbolehkan (beberapa memiliki kesalahpahaman bahwa mereka tidak) tetapi mereka harus dijaga agar tetap minimum.

Fungsi hash yang baik harus memetakan input yang diharapkan serata mungkin pada rentang outputnya. Itu harus memiliki keseragaman.

Mencegah DoS

Di .NET Core setiap kali Anda me-restart aplikasi, Anda akan mendapatkan kode hash yang berbeda. Ini adalah fitur keamanan untuk mencegah serangan Denial of Service (DoS). Untuk .NET Framework Anda harus mengaktifkan fitur ini dengan menambahkan file App.config berikut:

<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>

Karena fitur ini, kode hash tidak boleh digunakan di luar domain aplikasi tempat kode itu dibuat, kode hash tidak boleh digunakan sebagai bidang kunci dalam koleksi dan kode tersebut tidak boleh bertahan.

Baca lebih lanjut tentang ini di sini .

Aman Secara Kriptografis?

Algoritme tidak harus berupa fungsi hash Kriptografis . Artinya tidak harus memenuhi ketentuan berikut:

  • Tidak mungkin menghasilkan pesan yang menghasilkan nilai hash tertentu
  • Tidak mungkin menemukan dua pesan berbeda dengan nilai hash yang sama
  • Perubahan kecil pada pesan harus mengubah nilai hash secara ekstensif sehingga nilai hash baru tampaknya tidak berkorelasi dengan nilai hash lama (efek longsor).
Muhammad Rehan Saeed
sumber
29

Dalam kebanyakan kasus di mana Equals () membandingkan beberapa bidang, tidak masalah jika GetHash () Anda hash pada satu bidang atau banyak. Anda hanya perlu memastikan bahwa menghitung hash benar-benar murah ( Tidak ada alokasi , tolong) dan cepat ( Tidak ada perhitungan berat dan tentu saja tidak ada koneksi database) dan menyediakan distribusi yang baik.

Angkat berat harus menjadi bagian dari metode Equals (); hash harus menjadi operasi yang sangat murah untuk memungkinkan memanggil Persamaan () pada item sesedikit mungkin.

Dan satu tip terakhir: Jangan mengandalkan GetHashCode () yang stabil di atas beberapa aplikasi berjalan . Banyak tipe .Net tidak menjamin kode hash mereka untuk tetap sama setelah restart, jadi Anda hanya harus menggunakan nilai GetHashCode () untuk dalam struktur data memori.

Bert Huijben
sumber
10
"Dalam kebanyakan kasus di mana Equals () membandingkan beberapa bidang, tidak masalah jika GetHash () Anda hash pada satu bidang atau pada banyak bidang." Ini saran yang berbahaya, karena untuk objek yang hanya berbeda di bidang tanpa hash, Anda akan mendapatkan tabrakan hash. Jika ini sering terjadi, kinerja koleksi berbasis hash (HashMap, HashSet dll) akan menurun (hingga O (n) dalam kasus terburuk).
sleske
10
Ini sebenarnya terjadi di Java: Dalam versi awal dari JDK String.hashCode () hanya dianggap sebagai awal dari string; ini menyebabkan masalah kinerja jika Anda menggunakan Strings sebagai kunci di HashMaps yang hanya berbeda di bagian akhir (yang umum misalnya untuk URL). Algoritma itu diubah (dalam JDK 1.2 atau 1.3 saya percaya).
sleske
3
Jika satu bidang 'memberikan distribusi yang baik' (bagian terakhir dari jawaban saya), maka satu bidang sudah cukup .. Jika tidak memberikan distribusi yang baik , maka (dan saat itu) Anda memerlukan perhitungan lain. (Misalnya hanya menggunakan bidang lain yang tidak memberikan distribusi yang baik, atau menggunakan beberapa bidang)
Bert Huijben
Saya tidak berpikir ada masalah dengan GetHashCodemelakukan alokasi memori, asalkan itu hanya melakukannya pertama kali digunakan (dengan doa berikutnya hanya mengembalikan hasil cache). Yang penting bukanlah bahwa seseorang harus berusaha keras untuk menghindari tabrakan, melainkan bahwa ia harus menghindari tabrakan "sistemik". Jika suatu tipe memiliki dua intbidang oldXdan newXyang sering berbeda satu, nilai hash oldX^newXakan menetapkan 90% dari nilai hash rekaman tersebut dari 1, 2, 4, atau 8. Menggunakan oldX+newX[hitung aritmatika] dapat menghasilkan lebih banyak tabrakan ...
supercat
1
... daripada fungsi yang lebih canggih, tetapi koleksi 1.000.000 hal yang memiliki 500.000 nilai hash yang berbeda akan sangat baik jika setiap nilai hash memiliki dua hal yang terkait, dan sangat buruk jika satu nilai hash memiliki 500.001 hal dan yang lain memiliki masing-masing.
supercat
23

Sampai baru-baru ini jawaban saya akan sangat dekat dengan Jon Skeet di sini. Namun, saya baru-baru ini memulai sebuah proyek yang menggunakan tabel hash power-of-two, yaitu tabel hash di mana ukuran tabel internal adalah 8, 16, 32, dll. Ada alasan bagus untuk memilih ukuran bilangan prima, tetapi ada ada beberapa kelebihan pada power-of-two size juga.

Dan itu cukup menyebalkan. Jadi setelah sedikit percobaan dan penelitian, saya mulai mem-hashing hash saya dengan yang berikut:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

Dan kemudian tabel hash power-of-two saya tidak lagi menyedot.

Ini mengganggu saya, karena hal di atas seharusnya tidak berfungsi. Atau lebih tepatnya, itu tidak akan berfungsi kecuali yang asli GetHashCode()buruk dengan cara yang sangat khusus.

Mencampur ulang kode hash tidak dapat meningkatkan kode hash yang hebat, karena satu-satunya efek yang mungkin adalah bahwa kami memperkenalkan beberapa tabrakan lagi.

Mencampurkan kembali kode hash tidak dapat meningkatkan kode hash yang mengerikan, karena satu-satunya efek yang mungkin adalah kita mengubah misalnya sejumlah besar tabrakan pada nilai 53 ke sejumlah besar nilai 18,3487.291.

Mencampurkan kembali kode hash hanya dapat meningkatkan kode hash yang melakukan setidaknya cukup baik dalam menghindari tabrakan absolut sepanjang rentangnya (2 32 nilai yang mungkin) tetapi sangat buruk dalam menghindari tabrakan ketika modulo down untuk penggunaan aktual dalam tabel hash. Sementara modulo sederhana dari tabel power-of-two membuat ini lebih jelas, itu juga memiliki efek negatif dengan tabel bilangan prima yang lebih umum, yang tidak begitu jelas (kerja ekstra dalam pengulangan akan lebih besar daripada manfaatnya , tetapi manfaatnya tetap ada).

Sunting: Saya juga menggunakan pengalamatan terbuka, yang juga akan meningkatkan sensitivitas terhadap tabrakan, mungkin lebih daripada fakta bahwa itu adalah kekuatan dua.

Dan yah, itu mengganggu berapa banyak string.GetHashCode()implementasi di .NET (atau belajar di sini ) dapat ditingkatkan dengan cara ini (pada urutan tes berjalan sekitar 20-30 kali lebih cepat karena lebih sedikit tabrakan) dan lebih mengganggu berapa banyak kode hash saya sendiri dapat ditingkatkan (lebih dari itu).

Semua implementasi GetHashCode () yang saya kodekan di masa lalu, dan memang digunakan sebagai dasar jawaban di situs ini, jauh lebih buruk daripada yang saya bayangkan . Sebagian besar waktu itu "cukup baik" untuk banyak kegunaan, tetapi saya menginginkan sesuatu yang lebih baik.

Jadi saya meletakkan proyek itu di satu sisi (itu adalah proyek kesayangan) dan mulai mencari cara untuk menghasilkan kode hash yang baik dan didistribusikan dengan baik di .NET dengan cepat.

Pada akhirnya saya memutuskan untuk memindahkan SpookyHash ke .NET. Memang kode di atas adalah versi jalur cepat menggunakan SpookyHash untuk menghasilkan output 32-bit dari input 32-bit.

Sekarang, SpookyHash bukanlah cepat untuk mengingat sepotong kode. Port saya lebih kurang karena saya menggunakan banyak itu untuk kecepatan yang lebih baik *. Tapi untuk itulah penggunaan kembali kode.

Lalu aku menaruh bahwa proyek ke satu sisi, karena seperti proyek asli telah menghasilkan pertanyaan tentang bagaimana untuk menghasilkan kode hash yang lebih baik, sehingga proyek yang menghasilkan pertanyaan tentang bagaimana untuk menghasilkan yang lebih baik NET memcpy.

Kemudian saya kembali, dan menghasilkan banyak kelebihan untuk dengan mudah memberi makan hampir semua jenis asli (kecuali decimal†) ke dalam kode hash.

Ini cepat, di mana Bob Jenkins layak mendapatkan sebagian besar kredit karena kode aslinya yang saya porting masih lebih cepat, terutama pada mesin 64-bit yang algoritmanya dioptimalkan untuk ‡.

Kode lengkap dapat dilihat di https://bitbucket.org/JonHanna/spookilysharp/src tetapi pertimbangkan bahwa kode di atas adalah versi yang disederhanakan.

Namun, karena sekarang sudah ditulis, orang dapat menggunakannya dengan lebih mudah:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

Ini juga membutuhkan nilai seed, jadi jika Anda perlu berurusan dengan input yang tidak dipercaya dan ingin melindungi terhadap serangan Hash DoS, Anda dapat mengatur seed berdasarkan waktu kerja atau sejenisnya, dan membuat hasilnya tidak dapat diprediksi oleh penyerang:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* Kejutan besar dalam hal ini adalah dengan menggunakan metode rotasi yang mengembalikan (x << n) | (x >> -n)hal-hal yang ditingkatkan. Saya akan yakin bahwa jitter akan menjelaskan itu untuk saya, tetapi profiling menunjukkan sebaliknya.

decimalbukan asli dari perspektif .NET meskipun berasal dari C #. Masalah dengan itu adalah bahwa GetHashCode()memperlakukan sendiri presisi sebagai signifikan sedangkan miliknya Equals()tidak. Keduanya merupakan pilihan yang valid, tetapi tidak tercampur seperti itu. Dalam mengimplementasikan versi Anda sendiri, Anda harus memilih untuk melakukan satu, atau yang lain, tetapi saya tidak tahu yang Anda inginkan.

‡ Sebagai perbandingan. Jika digunakan pada string, SpookyHash pada 64 bit jauh lebih cepat daripada string.GetHashCode()pada 32 bit yang sedikit lebih cepat daripada string.GetHashCode()pada 64 bit, yang jauh lebih cepat daripada SpookyHash pada 32 bit, meskipun masih cukup cepat menjadi pilihan yang masuk akal.

Jon Hanna
sumber
Saat menggabungkan beberapa nilai hash menjadi satu, saya cenderung menggunakan longnilai untuk hasil antara, dan kemudian mengurangkan hasil akhir menjadi sebuah int. Apakah itu sepertinya ide yang bagus? Kekhawatiran saya adalah bahwa seseorang menggunakan mis hash = (hash * 31) + nextField, maka pasangan nilai-nilai yang cocok hanya akan mempengaruhi 27 bit atas dari hash. Membiarkan perhitungan meluas ke longdan membungkus barang-barang akan meminimalkan bahaya itu.
supercat
@supercat tergantung pada distribusi munging terakhir Anda. Pustaka SpookilySharp akan memastikan bahwa distribusinya baik, idealnya (karena tidak memerlukan penciptaan objek) dengan melewatkan sebuah pointer ke tipe yang dapat ditembus, atau melewati salah satu enumerable yang ditangani secara langsung, tetapi jika Anda belum memiliki blittable data atau enumerasi yang sesuai, kemudian memanggil .Update()dengan beberapa nilai sesuai jawaban di atas akan melakukan trik.
Jon Hanna
@JonHanna apakah Anda bersedia untuk lebih tepat dengan perilaku bermasalah yang Anda temui? Saya mencoba untuk mengimplementasikan perpustakaan yang membuat mengimplementasikan objek nilai sepele ( ValueUtils ) dan saya suka testset yang menunjukkan kemampuan hash yang buruk dalam kekuatan dari dua hashtable.
Eamon Nerbonne
@EamonNerbonne Saya tidak benar-benar memiliki sesuatu yang lebih tepat daripada "waktu keseluruhan lebih lambat seperti itu". Seperti yang saya tambahkan dalam edit, fakta bahwa saya menggunakan pengalamatan terbuka mungkin lebih penting daripada faktor kekuatan dua. Saya berencana untuk melakukan beberapa uji kasus pada proyek tertentu di mana saya akan membandingkan beberapa pendekatan yang berbeda, jadi saya mungkin memiliki jawaban yang lebih baik untuk Anda setelah itu, meskipun itu bukan prioritas tinggi (proyek pribadi tanpa kebutuhan mendesak) , jadi saya akan membahasnya ketika saya sampai ke sana ...)
Jon Hanna
@ JonHanna: ya saya tahu bagaimana jadwal proyek pribadi berjalan - semoga berhasil! Bagaimanapun, saya melihat saya tidak mengucapkan komentar terakhir dengan baik: saya bermaksud meminta masukan yang bermasalah, dan belum tentu perincian masalah yang dihasilkan. Saya ingin menggunakannya sebagai set tes (atau inspirasi untuk set tes). Dalam hal apapun - semoga sukses dengan proyek kesayangan Anda :-).
Eamon Nerbonne
13

Ini bagus:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

Dan inilah cara menggunakannya:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}
Magnus
sumber
1
Bagaimana Kunci ditentukan? GetHashCode () tidak mengambil parameter apa pun, jadi perlu memanggil yang ini dengan dua Tombol yang perlu ditentukan. Maaf, tanpa penjelasan lebih lanjut ini hanya terlihat pintar, tetapi tidak terlalu bagus.
Michael Stum
Dan mengapa Anda perlu overload generik? Tipe ini tidak penting (dan tidak digunakan dalam kode Anda) karena semua objek memiliki GetHashCode()metode, jadi Anda selalu dapat menggunakan metode dengan paramsparameter array. Atau saya kehilangan sesuatu di sini?
gehho
4
Saat Anda menggunakan objek alih-alih obat generik, Anda akan mendapatkan alokasi tinju dan memori, yang tidak Anda inginkan di GetHashCode. Jadi obat generik adalah jalan yang harus ditempuh.
CodesInChaos
1
Trailing shift / xor langkah ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);memiliki codesmell: mereka tidak bergantung pada salah input dan terlihat sangat membazir dengan saya.
sehe
1
@ Magnus ya benar, saya akan menghapus komentar asli saya. Hanya sedikit catatan bahwa ini mungkin tidak secepat solusi lain di sini, tetapi seperti yang Anda katakan tidak masalah. Distribusi sangat bagus, lebih baik daripada kebanyakan solusi di sini, jadi beri +1 dari saya! :)
nawfal
11

Pada https://github.com/dotnet/coreclr/pull/14863 , ada cara baru untuk menghasilkan kode hash yang super sederhana! Cukup tulis

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

Ini akan menghasilkan kode hash yang berkualitas tanpa Anda harus khawatir tentang detail implementasi.

James Ko
sumber
Itu terlihat seperti tambahan yang manis ... cara apa pun untuk mengetahui versi .NET Core apa yang akan dikirimkan?
Dan J
1
@DanJ Benar-benar kebetulan yang menyenangkan, HashCodeperubahan untuk corefx digabung hanya beberapa jam sebelum komentar Anda :) Tipe ini dijadwalkan untuk dikirimkan dalam .NET Core 2.1.
James Ko
Itu luar biasa - dan cukup waktu penyelesaiannya. Terpilih. :)
Dan J
@DanJ Bahkan berita yang lebih baik - itu harus tersedia saat ini di build CoreFX malam yang diinangi pada feed MyGet dotnet-core.
James Ko
Manis - yang tidak membantu saya di tempat kerja, karena kita tidak cukup bahwa pendarahan-tepi, tapi baik untuk tahu. Bersulang!
Dan J
9

Berikut ini adalah implementasi lain dari algoritma yang diposting di atas oleh Jon Skeet , tetapi tidak menyertakan alokasi atau operasi tinju:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

Pemakaian:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

Compiler akan memastikan HashValuetidak dipanggil dengan kelas karena batasan tipe generik. Tetapi tidak ada dukungan kompiler untuk HashObjectkarena menambahkan argumen generik juga menambahkan operasi tinju.

Scott Wegner
sumber
8

Ini pendekatan saya yang sederhana. Saya menggunakan pola pembangun klasik untuk ini. Ini adalah typesafe (tanpa tinju / unboxing) dan juga kompatibel dengan .NET 2.0 (tidak ada metode ekstensi dll.).

Digunakan seperti ini:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

Dan ini adalah kelas pembangun acutal:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}
bitbonk
sumber
Anda dapat menghindari pembuatan objek di dalam fungsi gethashcode seperti pada jawaban Mangus. Sebut saja fungsi hash statis sialan (yang peduli tentang hash starter). Selain itu, Anda bisa menggunakan AddItems<T>(params T[] items)metode lebih sering di kelas helper (daripada menelepon AddItem(T)setiap kali).
nawfal
Dan manfaat apa yang Anda temukan lakukan this.result * Prime2 * item.GetHashCode()ketika sering digunakan this.result * Prime2 + item.GetHashCode()?
nawfal
Saya tidak bisa menggunakan AddItems<T>(params T[] items)lebih sering karena typeof(T1) != typeof(T2)dll.
bitbonk
oh ya saya melewatkan itu.
nawfal
5

Pengguna ReSharper dapat membuat GetHashCode, Equals, dan lainnya dengan ReSharper -> Edit -> Generate Code -> Equality Members.

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}
Charles Burns
sumber
4

Jika kita memiliki tidak lebih dari 8 properti (semoga), berikut adalah alternatif lain.

ValueTupleadalah struct dan tampaknya memiliki GetHashCodeimplementasi yang solid .

Itu berarti kita bisa melakukan ini:

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

Mari kita lihat implementasi NET Core saat ini untuk ValueTuple's GetHashCode.

Ini dari ValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

Dan ini dari HashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

Dalam Bahasa Inggris:

  • Putar kiri (shift melingkar) h1 sebanyak 5 posisi.
  • Tambahkan hasilnya dan h1 bersamaan.
  • XOR hasilnya dengan h2.
  • Mulailah dengan melakukan operasi di atas pada {static random seed, h1}.
  • Untuk setiap item selanjutnya, lakukan operasi pada hasil sebelumnya dan item berikutnya (mis. H2).

Akan menyenangkan untuk mengetahui lebih banyak tentang properti dari algoritma hash code ROL-5 ini.

Sayangnya, menunda ValueTupleuntuk kita sendiri GetHashCodemungkin tidak secepat yang kita inginkan dan harapkan. Komentar ini dalam diskusi terkait menggambarkan bahwa panggilan langsung HashHelpers.Combinelebih berprestasi. Di sisi lain, itu internal, jadi kita harus menyalin kode, mengorbankan banyak dari apa yang kita dapatkan di sini. Selain itu, kami akan bertanggung jawab untuk mengingat terlebih dahulu Combinedengan benih acak. Saya tidak tahu apa konsekuensinya jika kita melewatkan langkah itu.

Timo
sumber
Dengan asumsi h1 >> 270 untuk mengabaikannya, h1 << 5sama dengan h1 * 32itu sama dengan h1 * 33 ^ h2. Menurut halaman ini , itu disebut "Modified Bernstein".
cactuaroid
3

Sebagian besar pekerjaan saya dilakukan dengan konektivitas database yang berarti bahwa semua kelas saya memiliki pengidentifikasi unik dari database. Saya selalu menggunakan ID dari database untuk menghasilkan kode hash.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}
Markus G
sumber
Itu berarti bahwa jika Anda memiliki objek Person dan Akun dan keduanya memiliki dan ID = 1, mereka akan memiliki kode hash yang sama. Dan itu tidak baik.
pero
15
Sebenarnya komentar di atas salah. Akan selalu ada kemungkinan tabrakan kode hash (kode hash hanya menempatkan bucket, bukan objek individual). Jadi implementasi seperti itu - untuk kode hash yang berisi objek campuran - akan menyebabkan banyak tabrakan, yang tidak diinginkan, tetapi akan baik-baik saja jika Anda hanya memiliki objek tipe tunggal di hashtables Anda. Juga tidak mendistribusikan secara merata, namun implementasi basis pada system.object juga, jadi saya tidak akan terlalu khawatir tentang hal itu ...
piers7
2
Kode hash bisa jadi id, karena id adalah bilangan bulat. Tidak perlu memanggil GetHashCode pada integer (ini merupakan fungsi identitas)
Darrel Lee
2
@ DarrelLee tapi tomo _id-nya bisa menjadi Guid. Ini adalah praktik pengkodean yang baik untuk dilakukan _id.GetHashCodekarena maksudnya jelas.
nawfal
2
@ 1224 tergantung pada pola penggunaannya bisa mengerikan karena alasan yang Anda berikan, tetapi juga bisa menjadi hebat; jika Anda memiliki urutan angka seperti itu tanpa lubang, maka Anda memiliki hash yang sempurna, lebih baik daripada algoritma apa pun yang bisa dihasilkan. Jika Anda tahu itu masalahnya, Anda bahkan dapat mengandalkannya dan melewatkan pemeriksaan kesetaraan.
Jon Hanna
3

Cukup mirip dengan solusi nightcoder kecuali lebih mudah untuk meningkatkan bilangan prima jika Anda mau.

PS: Ini adalah salah satu saat di mana Anda muntah sedikit di mulut, mengetahui bahwa ini dapat di refactored menjadi satu metode dengan 9 metode baku tetapi akan lebih lambat, jadi Anda hanya menutup mata dan mencoba melupakannya.

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}
Dbl
sumber
2
Tidak menangani nulls.
JJS
1

Saya mengalami masalah dengan mengapung dan desimal menggunakan implementasi yang dipilih sebagai jawaban di atas.

Tes ini gagal (mengapung; hash sama meskipun saya mengubah 2 nilai menjadi negatif):

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Tetapi tes ini lolos (dengan int):

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Saya mengubah implementasi saya untuk tidak menggunakan GetHashCode untuk tipe primitif dan sepertinya berfungsi lebih baik

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }
HokieMike
sumber
1
Dalam kasus Anda dimaksudkan dinyatakan uncheckedTIDAK mempengaruhi Convert.ToInt32: uint, long, float, doubledan decimalsemua bisa meluap di sini.
Mark Hurd
1

Microsoft memimpin untuk beberapa cara ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

Saya bisa menebak bahwa untuk beberapa int besar Anda dapat menggunakan ini:

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

Dan sama untuk multi-type: semua dikonversi terlebih dahulu untuk intdigunakan GetHashCode() maka nilai int akan di-xor'ed dan hasilnya adalah hash Anda.

Bagi mereka yang menggunakan hash sebagai ID (maksud saya nilai unik), hash secara alami terbatas pada sejumlah digit, saya pikir itu 5 byte untuk algoritma hashing, setidaknya MD5.

Anda dapat mengubah beberapa nilai menjadi nilai hash dan beberapa di antaranya sama, jadi jangan gunakan itu sebagai pengidentifikasi. (mungkin suatu hari nanti saya akan menggunakan komponen Anda)

deadManN
sumber
7
Bilangan bulat Xoring untuk membuat kode hash adalah antipattern terkenal yang cenderung menghasilkan sejumlah besar tabrakan dengan nilai-nilai dunia nyata.
Jon Hanna
Setiap orang di sini menggunakan integer, dan tidak pernah ada jaminan apa pun untuk hash sama, itu hanya mencoba menjadi sangat bervariasi karena ada beberapa tabrakan yang akan terjadi.
deadManN
Ya, tetapi yang kedua dan kelima Anda jangan mencoba menghindari tabrakan.
Jon Hanna
1
Ya, antipattern itu cukup umum.
Jon Hanna
2
Ada keseimbangan untuk dicapai. Gunakan kode hash yang benar-benar bagus seperti Spookyhash dan Anda akan mendapatkan banyak, menghindari tabrakan yang jauh lebih baik tetapi akan memiliki waktu perhitungan yang jauh lebih banyak daripada semua ini (tetapi ketika menyangkut hashing jumlah data yang sangat besar, Spookyhash sangat cepat). Pergeseran sederhana pada salah satu nilai sebelum xoring hanyalah biaya tambahan marjinal untuk pengurangan tabrakan yang baik. Penggandaan bilangan prima meningkatkan waktu dan kualitas lagi. Yang lebih baik antara shift atau mult karenanya dapat diperdebatkan. Xor polos meskipun sangat sering memiliki banyak tabrakan pada data nyata dan sebaiknya dihindari
Jon Hanna
1

Ini adalah kelas pembantu statis yang mengimplementasikan implementasi Josh Bloch; dan memberikan kelebihan secara eksplisit untuk "mencegah" tinju, dan juga untuk mengimplementasikan hash khusus untuk primitif panjang.

Anda dapat melewati perbandingan string yang cocok dengan implementasi yang sama dengan Anda.

Karena output Hash selalu merupakan int, Anda hanya dapat membuat panggilan Hash.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}
Steven Coco
sumber
Yipes: Saya menemukan bug! The HashKeysAndValuesMetode telah diperbaiki: itu memanggil HashKeyAndValue.
Steven Coco
0

Jika Anda ingin melakukan polyfill HashCodedarinetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

Catatan: Jika digunakan dengan struct, itu akan mengalokasikan memori karena tinju

Ivan Sanz-Carasa
sumber