Karena saya tidak dapat menemukan jawaban yang menjelaskan mengapa kita harus mengganti GetHashCode
dan Equals
untuk custom structs dan mengapa implementasi default "sepertinya tidak cocok untuk digunakan sebagai kunci dalam tabel hash", saya akan meninggalkan tautan ke blog ini posting , yang menjelaskan mengapa dengan contoh kasus nyata dari masalah yang terjadi.
Saya sarankan membaca seluruh posting, tetapi di sini adalah ringkasan (penekanan dan klarifikasi ditambahkan).
Alasan hash default untuk struct lambat dan tidak terlalu baik:
Cara CLR dirancang, setiap panggilan ke anggota yang ditentukan dalam System.ValueType
atau System.Enum
mengetik [dapat] menyebabkan alokasi tinju [...]
Seorang pelaksana fungsi hash menghadapi dilema: membuat distribusi fungsi hash yang baik atau membuatnya cepat. Dalam beberapa kasus, mungkin untuk mencapai mereka berdua, tetapi sulit untuk melakukan hal ini umum di ValueType.GetHashCode
.
Fungsi hash kanonik dari struct "menggabungkan" kode hash dari semua bidang. Tetapi satu-satunya cara untuk mendapatkan kode hash dari suatu bidang dalam ValueType
metode adalah dengan menggunakan refleksi . Jadi, penulis CLR memutuskan untuk berdagang kecepatan atas distribusi dan GetHashCode
versi default hanya mengembalikan kode hash dari bidang non-nol pertama dan "munges" dengan tipe id [...] Ini adalah perilaku yang wajar kecuali jika tidak . Misalnya, jika Anda kurang beruntung dan bidang pertama struct Anda memiliki nilai yang sama untuk sebagian besar contoh, maka fungsi hash akan memberikan hasil yang sama sepanjang waktu. Dan, seperti yang Anda bayangkan, ini akan menyebabkan dampak kinerja yang drastis jika instance ini disimpan dalam hash set atau tabel hash.
[...] Implementasi berbasis refleksi lambat . Sangat lambat.
[...] Keduanya ValueType.Equals
dan ValueType.GetHashCode
memiliki optimasi khusus. Jika suatu tipe tidak memiliki "pointer" dan dikemas dengan benar [...] maka versi yang lebih optimal digunakan: GetHashCode
iterates atas instance dan XOR blok 4 byte dan Equals
metode membandingkan dua instance menggunakan memcmp
. [...] Tetapi pengoptimalannya sangat rumit. Pertama, sulit untuk mengetahui kapan optimasi diaktifkan [...] Kedua, perbandingan memori tidak selalu memberi Anda hasil yang benar . Berikut adalah contoh sederhana: [...] -0.0
dan +0.0
sama tetapi memiliki representasi biner yang berbeda.
Masalah dunia nyata yang dijelaskan dalam pos:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Kami menggunakan tuple yang berisi struct kustom dengan implementasi kesetaraan default. Dan sayangnya, struct memiliki bidang pertama opsional yang hampir selalu sama dengan [string kosong] . Performanya OK sampai jumlah elemen dalam set meningkat secara signifikan menyebabkan masalah kinerja nyata, mengambil menit untuk menginisialisasi koleksi dengan puluhan ribu item.
Jadi, untuk menjawab pertanyaan "dalam kasus apa saya harus mengemas sendiri dan dalam kasus apa saya dapat dengan aman mengandalkan implementasi default", setidaknya dalam kasus struct , Anda harus mengganti Equals
dan GetHashCode
kapan pun struct kustom Anda dapat digunakan sebagai kunci dalam tabel hash atau Dictionary
.
Saya juga merekomendasikan menerapkan IEquatable<T>
dalam hal ini, untuk menghindari tinju.
Seperti jawaban lain mengatakan, jika Anda menulis kelas , hash default menggunakan referensi kesetaraan biasanya baik-baik saja, jadi saya tidak akan repot dalam hal ini, kecuali jika Anda perlu menimpa Equals
(maka Anda harus menimpa yang GetHashCode
sesuai).