Menggunakan bidang objek sebagai kunci kamus umum

120

Jika saya ingin menggunakan objek sebagai kunci untuk a Dictionary, metode apa yang perlu saya timpa untuk membandingkannya dengan cara tertentu?

Katakanlah saya memiliki kelas yang memiliki properti:

class Foo {
    public string Name { get; set; }
    public int FooID { get; set; }

    // elided
} 

Dan saya ingin membuat:

Dictionary<Foo, List<Stuff>>

Saya ingin Fooobjek dengan yang sama FooIDdianggap grup yang sama. Metode apa yang perlu saya ganti di Fookelas?

Untuk meringkas: Saya ingin mengkategorikan Stuffobjek ke dalam daftar, dikelompokkan berdasarkan Fooobjek. Stuffobjek akan memiliki FooIDuntuk menautkannya ke kategorinya.

Dana
sumber

Jawaban:

151

Secara default, dua metode penting adalah GetHashCode()dan Equals(). Penting bahwa jika dua hal sama ( Equals()mengembalikan true), keduanya memiliki kode hash yang sama. Misalnya, Anda mungkin "mengembalikan FooID;" seolah- GetHashCode()olah Anda ingin itu sebagai pertandingan. Anda juga dapat menerapkan IEquatable<Foo>, tetapi itu opsional:

class Foo : IEquatable<Foo> {
    public string Name { get; set;}
    public int FooID {get; set;}

    public override int GetHashCode() {
        return FooID;
    }
    public override bool Equals(object obj) {
        return Equals(obj as Foo);
    }
    public bool Equals(Foo obj) {
        return obj != null && obj.FooID == this.FooID;
    }
}

Terakhir, alternatif lain adalah menyediakan IEqualityComparer<T>untuk melakukan hal yang sama.

Marc Gravell
sumber
5
+1 dan saya tidak bermaksud membajak utas ini tetapi saya mendapat kesan bahwa GetHashCode () harus mengembalikan FooId.GetHashCode (). Bukankah ini pola yang benar?
Ken Browning
8
@Ken - yah, itu hanya perlu mengembalikan int yang menyediakan fitur yang diperlukan. FooID mana yang akan melakukan sebaik FooID.GetHashCode (). Sebagai detail implementasi, Int32.GetHashCode () adalah "return this;". Untuk tipe lain (string dll), maka yes: .GetHashCode () akan sangat berguna.
Marc Gravell
2
Terima kasih! Saya menggunakan IEqualityComparer <T> karena hanya untuk Dicarionary saya memerlukan metode yang diganti.
Dana
1
Anda harus menyadari bahwa performa container berdasarkan hashtable (Dictionary <T>, Dictionary, HashTable, dll.) Bergantung pada kualitas fungsi hash yang digunakan. Jika Anda hanya menggunakan FooID sebagai kode hash, container mungkin akan berkinerja sangat buruk.
Jørgen Fogh
2
@ JørgenFogh Saya sangat menyadari itu; contoh yang disajikan konsisten dengan maksud yang dinyatakan. Ada banyak masalah terkait terkait keabadian hash; ID lebih jarang berubah daripada nama, dan biasanya unik dan dapat diandalkan serta indikator kesetaraan. Topik yang tidak sepele.
Marc Gravell
33

Karena Anda ingin FooIDmenjadi pengenal grup, Anda harus menggunakannya sebagai kunci dalam kamus, bukan sebagai objek Foo:

Dictionary<int, List<Stuff>>

Jika Anda akan menggunakan Fooobjek sebagai kunci, Anda hanya perlu mengimplementasikan metode GetHashCodedan Equalsuntuk hanya mempertimbangkan FooIDproperti. The Nameproperti hanya akan menjadi bobot mati sejauh Dictionaryprihatin, sehingga Anda akan hanya menggunakan Foosebagai pembungkus untuk int.

Oleh karena itu, lebih baik menggunakan FooIDnilainya secara langsung, dan kemudian Anda tidak perlu menerapkan apa pun karena Dictionarysudah mendukung menggunakan an intsebagai kunci.

Sunting:
Jika Anda tetap ingin menggunakan Fookelas sebagai kunci, IEqualityComparer<Foo>mudah untuk diterapkan:

public class FooEqualityComparer : IEqualityComparer<Foo> {
   public int GetHashCode(Foo foo) { return foo.FooID.GetHashCode(); }
   public bool Equals(Foo foo1, Foo foo2) { return foo1.FooID == foo2.FooID; }
}

Pemakaian:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());
Guffa
sumber
1
Lebih tepatnya, int sudah mendukung metode / antarmuka yang diperlukan untuk digunakan sebagai kunci. Kamus tidak memiliki pengetahuan langsung tentang int atau tipe lainnya.
Jim Mischel
Saya memikirkan tentang itu, tetapi karena berbagai alasan, itu lebih bersih dan lebih nyaman menggunakan objek sebagai kunci kamus.
Dana
1
Yah, sepertinya Anda hanya menggunakan objek sebagai kunci, karena Anda sebenarnya hanya menggunakan id sebagai kunci.
Guffa
8

Untuk Foo Anda perlu mengganti object.GetHashCode () dan object.Equals ()

Dictionary akan memanggil GetHashCode () untuk menghitung keranjang hash untuk setiap nilai dan Sama dengan untuk membandingkan apakah dua Foo identik.

Pastikan untuk menghitung kode hash yang baik (hindari banyak objek Foo yang sama memiliki kode hash yang sama), tetapi pastikan dua Foos yang sama memiliki kode hash yang sama. Anda mungkin ingin memulai dengan Equals-Method dan kemudian (di GetHashCode ()) xatau kode hash dari setiap anggota yang Anda bandingkan di Equals.

public class Foo { 
     public string A;
     public string B;

     override bool Equals(object other) {
          var otherFoo = other as Foo;
          if (otherFoo == null)
             return false;
          return A==otherFoo.A && B ==otherFoo.B;
     }

     override int GetHashCode() {
          return 17 * A.GetHashCode() + B.GetHashCode();
     }
}
froh42
sumber
2
Selain - tetapi xor (^) membuat kombinator yang buruk untuk kode-hash, karena sering menyebabkan banyak tabrakan diagonal (yaitu {"foo", "bar"} vs {"bar", "foo"}. pilihannya adalah mengalikan dan menambahkan setiap suku - yaitu 17 * a. GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Marc, saya mengerti maksud Anda. Tapi bagaimana Anda bisa mendapatkan angka ajaib 17? Apakah menguntungkan menggunakan bilangan prima sebagai pengali untuk menggabungkan hash? Jika ya, mengapa?
froh42
Bolehkah saya menyarankan untuk mengembalikan: (A + B) .GetHashCode () daripada: 17 * A.GetHashCode () + B.GetHashCode () Ini akan: 1) Kecil kemungkinan mengalami tabrakan dan 2) memastikan bahwa tidak ada bilangan bulat melimpah.
Charles Burns
(A + B) .GetHashCode () membuat algoritme hashing yang sangat buruk, karena set (A, B) yang berbeda dapat menghasilkan hash yang sama jika digabungkan ke string yang sama; "hellow" + "ned" sama dengan "neraka" + "milik" dan akan menghasilkan hash yang sama.
kaesve
@kaesve bagaimana dengan (A + "" + B) .GetHashCode ()?
Abadi
1

Bagaimana dengan Hashtablekelas!

Hashtable oMyDic = new Hashtable();
Object oAnyKeyObject = null;
Object oAnyValueObject = null;
oMyDic.Add(oAnyKeyObject, oAnyValueObject);
foreach (DictionaryEntry de in oMyDic)
{
   // Do your job
}

Dengan cara di atas, Anda dapat menggunakan objek apa pun (objek kelas Anda) sebagai kunci Kamus generik :)

Behzad Ebrahimi
sumber
1

Saya memiliki masalah yang sama. Saya sekarang dapat menggunakan objek apa pun yang saya coba sebagai kunci karena menimpa Equals dan GetHashCode.

Berikut adalah kelas yang saya bangun dengan metode untuk digunakan di dalam menimpa Equals (object obj) dan GetHashCode (). Saya memutuskan untuk menggunakan generik dan algoritma hashing yang seharusnya dapat mencakup sebagian besar objek. Beri tahu saya jika Anda melihat sesuatu di sini yang tidak berfungsi untuk beberapa jenis objek dan Anda punya cara untuk memperbaikinya.

public class Equality<T>
{
    public int GetHashCode(T classInstance)
    {
        List<FieldInfo> fields = GetFields();

        unchecked
        {
            int hash = 17;

            foreach (FieldInfo field in fields)
            {
                hash = hash * 397 + field.GetValue(classInstance).GetHashCode();
            }
            return hash;
        }
    }

    public bool Equals(T classInstance, object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }
        if (ReferenceEquals(this, obj))
        {
            return true;
        }
        if (classInstance.GetType() != obj.GetType())
        {
            return false;
        }

        return Equals(classInstance, (T)obj);
    }

    private bool Equals(T classInstance, T otherInstance)
    {
        List<FieldInfo> fields = GetFields();

        foreach (var field in fields)
        {
            if (!field.GetValue(classInstance).Equals(field.GetValue(otherInstance)))
            {
                return false;
            }
        }

        return true;
    }

    private List<FieldInfo> GetFields()
    {
        Type myType = typeof(T);

        List<FieldInfo> fields = myType.GetTypeInfo().DeclaredFields.ToList();
        return fields;
    }
}

Berikut cara penggunaannya di kelas:

public override bool Equals(object obj)
    {
        return new Equality<ClassName>().Equals(this, obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return new Equality<ClassName>().GetHashCode(this);
        }
    }
kb4000
sumber