Jika kode hash nol selalu nol, dalam .NET

88

Mengingat bahwa koleksi seperti System.Collections.Generic.HashSet<>menerima nullsebagai anggota set, orang dapat bertanya apa kode hash yang nullseharusnya. Sepertinya kerangka tersebut menggunakan 0:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

Ini bisa (sedikit) bermasalah dengan enum nullable. Jika kita mendefinisikan

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

maka Nullable<Season>(juga disebut Season?) dapat mengambil hanya lima nilai, tetapi dua di antaranya, yaitu nulldan Season.Spring, memiliki kode hash yang sama.

Sangat menggoda untuk menulis pembanding kesetaraan yang "lebih baik" seperti ini:

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

Tapi adakah alasan mengapa kode hash nullharus 0?

EDIT / TAMBAH:

Beberapa orang sepertinya berpikir ini tentang menimpa Object.GetHashCode(). Sebenarnya tidak. (Penulis NET memang membuat override dari GetHashCode()dalam Nullable<>struct yang merupakan relevan, meskipun.) Implementasi ditulis pengguna dari parameterless yang GetHashCode()tidak pernah dapat menangani situasi di mana objek yang kode hash yang kita cari adalah null.

Ini tentang mengimplementasikan metode abstrak EqualityComparer<T>.GetHashCode(T)atau mengimplementasikan metode antarmuka IEqualityComparer<T>.GetHashCode(T). Sekarang, saat membuat tautan ini ke MSDN, saya melihat bahwa dikatakan di sana bahwa metode ini melempar ArgumentNullExceptionjika satu-satunya argumen mereka null. Ini pasti kesalahan di MSDN? Tak satu pun dari implementasi .NET sendiri yang memunculkan pengecualian. Melemparkan dalam kasus itu secara efektif akan mematahkan setiap upaya untuk menambah nullke HashSet<>. Kecuali HashSet<>melakukan sesuatu yang luar biasa ketika berhadapan dengan suatu nullitem (saya harus mengujinya).

EDIT / TAMBAHAN BARU:

Sekarang saya mencoba debugging. Dengan HashSet<>, saya dapat mengonfirmasi bahwa dengan pembanding kesetaraan default, nilai Season.Springdan null akan berakhir di keranjang yang sama. Ini dapat ditentukan dengan sangat hati-hati memeriksa anggota array pribadi m_bucketsdan m_slots. Perhatikan bahwa indeks selalu, menurut desain, diimbangi satu.

Kode yang saya berikan di atas tidak, bagaimanapun, memperbaiki ini. Ternyata, HashSet<>bahkan tidak akan pernah meminta pembanding kesetaraan ketika nilainya null. Ini dari kode sumber HashSet<>:

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

Artinya, setidaknya untuk HashSet<>, bahkan tidak mungkin untuk mengubah hash null. Sebagai gantinya, solusinya adalah mengubah hash dari semua nilai lainnya, seperti ini:

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}
Jeppe Stig Nielsen
sumber
26
Mengapa kode hash untuk null tidak harus nol? Tabrakan hash bukanlah akhir dari dunia, lho.
Hot Licks
3
Kecuali bahwa itu tabrakan yang terkenal dan cukup umum. Bukan berarti itu buruk atau bahkan masalah besar, itu mudah dihindari
Chris Pfohl
8
lol mengapa saya berpikir "jika kerangka .NET melompat dari jembatan, apakah Anda akan mengikutinya?" ...
Adam Houldsworth
3
Hanya karena penasaran, apa jadinya musim nol?
SwDevMan81
1
Secara pribadi, inilah mengapa saya selalu memberi enum saya nilai "Empty" atau "Unknown" sebagai nilai pertama. Dengan cara ini, enum Musim saya tidak akan pernah mewakili Musim Semi tanpa saya secara eksplisit mengatur nilainya, sehingga meniadakan masalah.
Chris

Jawaban:

25

Selama kode hash yang dikembalikan untuk null konsisten dengan jenisnya, Anda akan baik-baik saja. Satu-satunya persyaratan untuk kode hash adalah bahwa dua objek yang dianggap sama memiliki kode hash yang sama.

Mengembalikan 0 atau -1 untuk null, selama Anda memilih satu dan mengembalikannya sepanjang waktu, akan berhasil. Jelas, kode hash non-null tidak boleh mengembalikan nilai apa pun yang Anda gunakan untuk null.

Pertanyaan serupa:

GetHashCode pada bidang null?

Apa yang harus dikembalikan GetHashCode ketika pengenal objek bernilai null?

"Keterangan" dari entri MSDN ini menjelaskan lebih detail seputar kode hash. Sayangnya, dokumentasi tidak memberikan liputan atau diskusi tentang nilai nol sama sekali - bahkan dalam konten komunitas.

Untuk mengatasi masalah Anda dengan enum, terapkan kembali kode hash untuk mengembalikan bukan nol, tambahkan entri enum default "tidak diketahui" yang setara dengan null, atau jangan gunakan enum nullable.

Ngomong-ngomong, temuan menarik.

Masalah lain yang saya lihat dengan ini umumnya adalah bahwa kode hash tidak dapat mewakili tipe 4 byte atau lebih besar yang nullable tanpa setidaknya satu tabrakan (lebih banyak karena ukuran tipe meningkat). Misalnya, kode hash dari sebuah int hanyalah int, jadi kode ini menggunakan rentang int lengkap. Nilai apa dalam rentang itu yang Anda pilih untuk null? Apa pun yang Anda pilih akan bertabrakan dengan kode hash nilai itu sendiri.

Tabrakan dalam dan dari dirinya sendiri tidak selalu menjadi masalah, tetapi Anda perlu tahu bahwa benturan itu ada. Kode hash hanya digunakan dalam beberapa keadaan. Seperti yang dinyatakan dalam dokumen di MSDN, kode hash tidak dijamin untuk mengembalikan nilai yang berbeda untuk objek yang berbeda sehingga seharusnya tidak diharapkan.

Adam Houldsworth
sumber
Saya tidak berpikir pertanyaan yang Anda tautkan sepenuhnya serupa. Saat Anda menimpa Object.GetHashCode()di kelas Anda sendiri (atau struct), Anda tahu bahwa kode ini hanya akan dipukul ketika orang benar-benar memiliki instance dari kelas Anda. Contoh itu tidak mungkin null. Itulah mengapa Anda tidak memulai penggantian Object.GetHashCode()dengan if (this == null) return -1;Ada perbedaan antara "menjadi null" dan "menjadi objek yang memiliki beberapa bidang yang ada null".
Jeppe Stig Nielsen
Anda berkata: Jelas, kode hash non-null seharusnya tidak mengembalikan nilai apa pun yang Anda gunakan untuk null. Itu akan ideal, saya setuju. Dan itulah alasan mengapa saya mengajukan pertanyaan saya di tempat pertama, karena setiap kali kita menulis enum T, maka (T?)nulldan (T?)default(T)akan memiliki kode hash yang sama (dalam implementasi .NET saat ini). Yang bisa diubah jika pelaksana dari NET berubah baik kode hash dari null atau algoritma kode hash dari System.Enum.
Jeppe Stig Nielsen
Saya setuju tautan itu untuk bidang internal nol. Anda menyebutkan itu untuk IEqualityComparer <T>, dalam implementasi Anda kode hash masih spesifik untuk suatu tipe sehingga Anda masih dalam situasi yang sama, konsistensi untuk tipe tersebut. Mengembalikan kode hash yang sama untuk null jenis apa pun tidak masalah karena null tidak memiliki jenis.
Adam Houldsworth
1
Catatan: Saya memperbarui pertanyaan saya dua kali. Ternyata (setidaknya dengan HashSet<>) itu tidak berfungsi untuk mengubah kode hash null.
Jeppe Stig Nielsen
6

Ingatlah bahwa kode hash digunakan sebagai langkah pertama dalam menentukan persamaan saja, dan [is / should] tidak pernah digunakan sebagai penentuan de-facto apakah dua objek sama.

Jika kode hash dua objek tidak sama maka mereka diperlakukan sebagai tidak sama (karena kami berasumsi bahwa implementasi yang tepat adalah benar - yaitu kami tidak menebak-nebak). Jika mereka memiliki kode hash yang sama, maka mereka kemudian harus diperiksa untuk persamaan aktual yang, dalam kasus Anda, nulldan nilai enum akan gagal.

Akibatnya - menggunakan nol sama baiknya dengan nilai lain dalam kasus umum.

Tentu, akan ada situasi, seperti enum Anda, di mana nol ini dibagikan dengan kode hash nilai nyata . Pertanyaannya adalah apakah, bagi Anda, overhead yang sangat kecil dari perbandingan tambahan menyebabkan masalah.

Jika demikian, maka tentukan pembanding Anda sendiri untuk kasus nullable untuk tipe tertentu Anda, dan pastikan bahwa nilai null selalu menghasilkan kode hash yang selalu sama (tentu saja!) Dan nilai yang tidak dapat dihasilkan oleh yang mendasarinya tipe algoritma kode hash sendiri. Untuk tipe Anda sendiri, ini bisa dilakukan. Untuk orang lain - semoga berhasil :)

Andras Zoltan
sumber
5

Tidak harus menjadi nol - Anda bisa membuat 42 jika Anda ingin.

Yang terpenting adalah konsistensi selama pelaksanaan program.

Itu hanya representasi yang paling jelas, karena nullsering direpresentasikan sebagai nol secara internal. Artinya, saat men-debug, jika Anda melihat kode hash nol, Anda mungkin akan berpikir, "Hmm .. apakah ini masalah referensi null?"

Perhatikan bahwa jika Anda menggunakan nomor seperti 0xDEADBEEF, maka seseorang dapat mengatakan Anda menggunakan nomor ajaib ... dan Anda seperti itu. (Anda bisa mengatakan nol adalah angka ajaib juga, dan Anda akan benar ... kecuali bahwa itu digunakan secara luas sehingga menjadi semacam pengecualian pada aturan tersebut.)

pengguna541686
sumber
4

Pertanyaan bagus.

Saya baru saja mencoba membuat kode ini:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

dan lakukan ini seperti ini:

Season? v = null;
Console.WriteLine(v);

itu kembali null

jika saya lakukan, bukannya normal

Season? v = Season.Spring;
Console.WriteLine((int)v);

itu kembali 0, seperti yang diharapkan, atau Spring sederhana jika kita menghindari transmisi ke int.

Jadi .. jika Anda melakukan hal berikut:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

EDIT

Dari MSDN

Jika dua objek dibandingkan sebagai sama, metode GetHashCode untuk setiap objek harus mengembalikan nilai yang sama. Namun, jika dua objek tidak dibandingkan sebagai sama, metode GetHashCode untuk dua objek tidak harus mengembalikan nilai yang berbeda.

Dengan kata lain: jika dua objek memiliki kode hash yang sama namun tidak berarti keduanya sama, penyebab persamaan nyata ditentukan oleh Equals .

Dari MSDN lagi:

Metode GetHashCode untuk sebuah objek harus secara konsisten mengembalikan kode hash yang sama selama tidak ada modifikasi pada status objek yang menentukan nilai kembalian dari metode Equals objek. Perhatikan bahwa ini benar hanya untuk eksekusi aplikasi saat ini, dan kode hash yang berbeda dapat dikembalikan jika aplikasi dijalankan lagi.

Tigran
sumber
6
tabrakan, menurut definisi, berarti dua objek yang tidak sama memiliki kode hash yang sama. Anda telah menunjukkan bahwa objek tidak sama. Sekarang apakah mereka memiliki kode hash yang sama? Menurut OP yang mereka lakukan, artinya ini adalah tabrakan. Sekarang, ini bukanlah akhir dari dunia untuk memiliki tabrakan, itu hanya tabrakan yang lebih mungkin terjadi daripada jika nol di-hash ke sesuatu selain 0, yang merusak kinerja.
Pelayanan
1
Jadi, apa jawaban Anda sebenarnya? Anda mengatakan bahwa Musim Semi tidak sama dengan nol. Yah, itu tidak salah, tetapi itu tidak benar-benar menjawab pertanyaan dengan cara apa pun sekarang.
Pelayanan
2
@ Servy: pertanyaannya mengatakan: itu sebabnya saya memiliki kode has yang sama untuk 2 objek yang berbeda ( null dan Spring ). Jadi jawabannya adalah bahwa tidak ada penyebab tabrakan meskipun memiliki kode hash yang sama, mereka tidak sama.
Tigran
3
"Jawaban: kenapa tidak?" Nah, OP terlebih dahulu menjawab pertanyaan Anda tentang "mengapa tidak". Ini lebih mungkin menyebabkan tabrakan daripada nomor lain. Dia bertanya-tanya apakah ada alasan 0 dipilih, dan sejauh ini tidak ada yang menjawabnya.
Pelayanan
1
Jawaban ini tidak mengandung apa pun yang belum diketahui OP, terbukti dari cara pertanyaan itu diajukan.
Konrad Rudolph
4

Tetapi apakah ada alasan mengapa kode hash nol harus 0?

Bisa jadi apa saja. Saya cenderung setuju bahwa 0 belum tentu pilihan terbaik, tetapi itu mungkin menyebabkan bug paling sedikit.

Fungsi hash mutlak harus mengembalikan hash yang sama untuk nilai yang sama. Setelah terdapat sebuah komponen yang melakukan ini, ini benar-benar nilai hanya berlaku untuk hash dari null. Jika ada konstanta untuk ini, seperti, hm object.HashOfNull, maka seseorang yang mengimplementasikan IEqualityComparerharus tahu untuk menggunakan nilai itu. Jika mereka tidak memikirkannya, kemungkinan mereka akan menggunakan 0 sedikit lebih tinggi daripada setiap nilai lainnya, saya rasa.

setidaknya untuk HashSet <>, bahkan tidak mungkin untuk mengubah hash nol

Seperti disebutkan di atas, saya pikir itu sama sekali tidak mungkin berhenti penuh, hanya karena ada jenis yang sudah mengikuti konvensi yang hash nol adalah 0.

Roman Starkov
sumber
Ketika seseorang mengimplementasikan metode EqualityComparer<T>.GetHashCode(T)untuk beberapa tipe tertentu Tyang memungkinkan null, dia harus melakukan sesuatu saat argumennya null. Anda bisa (1) melempar ArgumentNullException, (2) mengembalikan 0, atau (3) mengembalikan sesuatu yang lain. Saya mengambil jawaban Anda untuk rekomendasi selalu kembali 0dalam situasi itu?
Jeppe Stig Nielsen
@JeppeStigNielsen Saya tidak yakin tentang lemparan vs pengembalian, tetapi jika Anda memilih untuk kembali, maka pasti nol.
Roman Starkov
2

Ini adalah 0 demi kesederhanaan. Tidak ada persyaratan yang begitu sulit. Anda hanya perlu memastikan persyaratan umum pengkodean hash.

Misalnya, Anda perlu memastikan bahwa jika dua objek sama, kode hashnya juga harus sama. Oleh karena itu, kode hash yang berbeda harus selalu mewakili objek yang berbeda (tetapi belum tentu benar, sebaliknya: dua objek yang berbeda mungkin memiliki kode hash yang sama, meskipun jika ini sering terjadi maka ini bukan fungsi hash berkualitas baik - tidak memiliki ketahanan tabrakan yang baik).

Tentu saja, saya membatasi jawaban saya untuk persyaratan alam matematika. Ada juga kondisi teknis khusus .NET, yang dapat Anda baca di sini . 0 untuk nilai nol tidak ada di antara mereka.

Thomas Calc
sumber
1

Jadi ini bisa dihindari dengan menggunakan Unknownnilai enum (meskipun tampaknya agak aneh untuk Seasontidak diketahui). Jadi hal seperti ini akan meniadakan masalah ini:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

Kemudian Anda akan memiliki nilai kode hash unik untuk setiap musim.

SwDevMan81
sumber
1
ya, tapi ini sebenarnya bukan pertanyaannya. Dengan cara ini menurut pertanyaan null akan bertabrakan dengan Uknown. Apa itu differnce?
Tigran
@Tigran - Versi ini tidak menggunakan tipe nullable
SwDevMan81
Begitu, tapi pertanyaannya adalah tentang tipe nullable.
Tigran
Saya memiliki adegan jutaan kali di SO yang orang menawarkan saran untuk perbaikan sebagai jawaban.
SwDevMan81
1

Secara pribadi saya merasa menggunakan nilai nullable agak canggung dan mencoba menghindarinya kapan pun saya bisa. Masalah Anda hanyalah alasan lain. Kadang-kadang mereka sangat berguna tetapi aturan praktis saya adalah tidak mencampur tipe nilai dengan null jika memungkinkan hanya karena ini berasal dari dua dunia yang berbeda. Dalam kerangka .NET mereka tampaknya melakukan hal yang sama - banyak tipe nilai menyediakan TryParsemetode yang merupakan cara untuk memisahkan nilai dari tidak ada nilai ( null).

Dalam kasus khusus Anda, mudah untuk menyingkirkan masalah karena Anda menangani Seasontipe Anda sendiri .

(Season?)nullbagi saya berarti 'musim tidak ditentukan' seperti ketika Anda memiliki formulir web di mana beberapa bidang tidak diperlukan. Menurut pendapat saya, lebih baik untuk menetapkan 'nilai' khusus enumitu sendiri daripada menggunakan sedikit kikuk Nullable<T>. Ini akan lebih cepat (tanpa tinju) lebih mudah dibaca ( Season.NotSpecifiedvs null) dan akan menyelesaikan masalah Anda dengan kode hash.

Tentu saja untuk jenis lain, seperti intAnda tidak dapat memperluas domain nilai dan untuk menandakan salah satu nilai sebagai spesial tidak selalu memungkinkan. Tetapi dengan int?tabrakan kode hash adalah masalah yang jauh lebih kecil, jika sama sekali.

Maciej
sumber
Ketika Anda mengatakan "tinju", saya pikir yang Anda maksud adalah "membungkus", yaitu meletakkan nilai struct di dalam Nullable<>struct (di mana HasValueanggota akan diatur ke true). Apakah Anda yakin masalahnya benar-benar lebih kecil int?? Sering kali seseorang hanya menggunakan sedikit nilai int, dan kemudian itu setara dengan enum (yang secara teori dapat memiliki banyak anggota).
Jeppe Stig Nielsen
Secara umum saya akan mengatakan bahwa enum dipilih ketika ada sejumlah nilai yang diketahui yang diperlukan (2-10). Jika batas lebih besar atau tidak sama sekali, intlebih masuk akal. Tentu saja preferensinya berbeda-beda.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Denis535
sumber
1
Itu pendekatan yang menarik. Akan berguna untuk mengedit jawaban Anda untuk memasukkan beberapa penjelasan tambahan, dan terutama mengingat sifat pertanyaannya.
Jeremy Caney