Tentang pentingnya GetHashCode
Yang lain sudah berkomentar tentang fakta bahwa IEqualityComparer<T>
implementasi kustom harus benar-benar menyertakan GetHashCode
metode ; tapi tidak ada yang mau repot-repot menjelaskan alasannya .
Inilah sebabnya. Pertanyaan Anda secara khusus menyebutkan metode ekstensi LINQ; hampir semua ini mengandalkan kode hash untuk bekerja dengan baik, karena mereka menggunakan tabel hash secara internal untuk efisiensi.
Ambil Distinct
contohnya. Pertimbangkan implikasi metode ekstensi ini jika semua yang digunakan adalah Equals
metode. Bagaimana Anda menentukan apakah suatu item sudah dipindai secara berurutan jika hanya dimiliki Equals
? Anda menghitung seluruh koleksi nilai yang telah Anda lihat dan memeriksa kecocokan. Ini akan menghasilkan Distinct
penggunaan algoritma O (N 2 ) kasus terburuk alih-alih O (N)!
Untungnya, ini bukan masalahnya. Distinct
tidak hanya digunakan Equals
; itu menggunakan GetHashCode
juga. Bahkan, itu benar - benar tidak berfungsi sebagaimana mestinya tanpa IEqualityComparer<T>
pasokan yang tepatGetHashCode
. Di bawah ini adalah contoh buat menggambarkan ini.
Katakanlah saya memiliki tipe berikut:
class Value
{
public string Name { get; private set; }
public int Number { get; private set; }
public Value(string name, int number)
{
Name = name;
Number = number;
}
public override string ToString()
{
return string.Format("{0}: {1}", Name, Number);
}
}
Sekarang katakan saya punya List<Value>
dan saya ingin menemukan semua elemen dengan nama yang berbeda. Ini adalah kasus penggunaan yang sempurna untuk Distinct
menggunakan pembanding kesetaraan khusus. Jadi mari kita gunakan Comparer<T>
kelas dari jawaban Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Sekarang, jika kita memiliki banyak Value
elemen dengan Name
properti yang sama , semuanya harus runtuh menjadi satu nilai yang dikembalikan Distinct
, kan? Ayo lihat...
var values = new List<Value>();
var random = new Random();
for (int i = 0; i < 10; ++i)
{
values.Add("x", random.Next());
}
var distinct = values.Distinct(comparer);
foreach (Value x in distinct)
{
Console.WriteLine(x);
}
Keluaran:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, itu tidak berhasil, kan?
Bagaimana dengan GroupBy
? Mari kita coba itu:
var grouped = values.GroupBy(x => x, comparer);
foreach (IGrouping<Value> g in grouped)
{
Console.WriteLine("[KEY: '{0}']", g);
foreach (Value x in g)
{
Console.WriteLine(x);
}
}
Keluaran:
[KEY = 'x: 1346013431']
x: 1346013431
[KEY = 'x: 1388845717']
x: 1388845717
[KEY = 'x: 1576754134']
x: 1576754134
[KEY = 'x: 1104067189']
x: 1104067189
[KEY = 'x: 1144789201']
x: 1144789201
[KEY = 'x: 1862076501']
x: 1862076501
[KEY = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377
Lagi: tidak berhasil.
Jika Anda memikirkannya, masuk akal untuk Distinct
menggunakan HashSet<T>
(atau setara) secara internal, dan untuk GroupBy
menggunakan sesuatu seperti Dictionary<TKey, List<T>>
internal. Bisakah ini menjelaskan mengapa metode ini tidak berhasil? Mari kita coba ini:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Keluaran:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Ya ... mulai masuk akal?
Semoga dari contoh-contoh ini jelas mengapa memasukkan yang sesuai GetHashCode
dalam IEqualityComparer<T>
implementasi apa pun begitu penting.
Jawaban asli
Memperluas jawaban orip :
Ada beberapa perbaikan yang bisa dilakukan di sini.
- Pertama, saya akan mengambil
Func<T, TKey>
alih-alih Func<T, object>
; ini akan mencegah tinju kunci tipe nilai dalam keyExtractor
dirinya sendiri.
- Kedua, saya sebenarnya akan menambahkan
where TKey : IEquatable<TKey>
batasan; ini akan mencegah tinju dalam Equals
panggilan ( object.Equals
mengambil object
parameter; Anda perlu IEquatable<TKey>
implementasi untuk mengambil TKey
parameter tanpa tinju). Jelas ini bisa menimbulkan batasan yang terlalu berat, sehingga Anda bisa membuat kelas dasar tanpa batasan dan kelas turunan dengannya.
Seperti apa bentuk kode yang dihasilkan:
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
protected readonly Func<T, TKey> keyExtractor;
public KeyEqualityComparer(Func<T, TKey> keyExtractor)
{
this.keyExtractor = keyExtractor;
}
public virtual bool Equals(T x, T y)
{
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
public int GetHashCode(T obj)
{
return this.keyExtractor(obj).GetHashCode();
}
}
public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
where TKey : IEquatable<TKey>
{
public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
: base(keyExtractor)
{ }
public override bool Equals(T x, T y)
{
// This will use the overload that accepts a TKey parameter
// instead of an object parameter.
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
}
IEqualityComparer<T>
yangGetHashCode
keluar akan langsung rusak.