Tuple (atau array) sebagai kunci Kamus di C #


109

Saya mencoba membuat tabel pencarian Kamus di C #. Saya perlu menyelesaikan 3-tuple nilai menjadi satu string. Saya mencoba menggunakan array sebagai kunci, tetapi tidak berhasil, dan saya tidak tahu harus berbuat apa lagi. Pada titik ini saya sedang mempertimbangkan untuk membuat Dictionary of Dictionaries of Dictionaries, tetapi itu mungkin tidak terlalu bagus untuk dilihat, meskipun begitulah cara saya melakukannya dalam javascript.

Jawaban:


113

Jika Anda menggunakan .NET 4.0 menggunakan Tuple:

lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

Jika tidak, Anda dapat menentukan Tupledan menggunakannya sebagai kunci. Tuple perlu mengganti GetHashCode, Equalsdan IEquatable:

struct Tuple<T, U, W> : IEquatable<Tuple<T,U,W>>
{
    readonly T first;
    readonly U second;
    readonly W third;

    public Tuple(T first, U second, W third)
    {
        this.first = first;
        this.second = second;
        this.third = third;
    }

    public T First { get { return first; } }
    public U Second { get { return second; } }
    public W Third { get { return third; } }

    public override int GetHashCode()
    {
        return first.GetHashCode() ^ second.GetHashCode() ^ third.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        return Equals((Tuple<T, U, W>)obj);
    }

    public bool Equals(Tuple<T, U, W> other)
    {
        return other.first.Equals(first) && other.second.Equals(second) && other.third.Equals(third);
    }
}

6
Struct ini juga harus mengimplementasikan IEquatable <Tuple <T, U, W >>. Dengan cara itu Anda dapat menghindari tinju saat Equals () dipanggil dalam kasus benturan kode hash.
Dustin Campbell

16
@jerryjvl dan semua orang yang menemukan ini oleh Google seperti yang saya lakukan, Tuple .NET 4 mengimplementasikan sama sehingga dapat digunakan dalam kamus.
Scott Chamberlain

30
GetHashCodePenerapan Anda tidak terlalu baik. Itu tidak berubah di bawah permutasi bidang.
CodesInChaos

2
Tuple seharusnya bukan sebuah struct. Dalam kerangka kerja, Tuple adalah tipe referensi.
Michael Graczyk

5
@ Thoraot - tentu saja contoh Anda salah ... seharusnya begitu. Mengapa new object()sama dengan yang lain new object()? Ini tidak hanya menggunakan perbandingan referensi langsung ... coba:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Mike Marynowski

35

Antara pendekatan berbasis kamus tuple dan bersarang, hampir selalu lebih baik untuk menggunakan berbasis tupel.

Dari sudut pandang pemeliharaan ,

  • jauh lebih mudah untuk mengimplementasikan fungsionalitas yang terlihat seperti:

    var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

    dari

    var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();

    dari sisi callee. Dalam kasus kedua, setiap penambahan, pencarian, penghapusan, dll memerlukan tindakan pada lebih dari satu kamus.

  • Selain itu, jika kunci komposit Anda memerlukan satu bidang lagi (atau kurang) di masa mendatang, Anda perlu mengubah banyak kode dalam kasus kedua (kamus bertingkat) karena Anda harus menambahkan kamus bertingkat lebih lanjut dan pemeriksaan berikutnya.

Dari perspektif kinerja , kesimpulan terbaik yang bisa Anda capai adalah dengan mengukurnya sendiri. Tetapi ada beberapa batasan teoretis yang dapat Anda pertimbangkan sebelumnya:

  • Dalam kasus kamus bersarang, memiliki kamus tambahan untuk setiap kunci (luar dan dalam) akan memiliki beberapa overhead memori (lebih dari apa yang akan membuat tupel).

  • Dalam kasus kamus bersarang, setiap tindakan dasar seperti penambahan, pembaruan, pencarian, penghapusan, dll perlu dilakukan dalam dua kamus. Sekarang ada kasus di mana pendekatan kamus bersarang bisa lebih cepat, yaitu, ketika data yang dicari tidak ada, karena kamus perantara dapat melewati penghitungan & perbandingan kode hash lengkap, tetapi sekali lagi itu harus diatur waktunya untuk memastikan. Jika ada data, ini harus lebih lambat karena pencarian harus dilakukan dua kali (atau tiga kali bergantung pada penumpukan).

  • Mengenai pendekatan tupel, tupel .NET bukanlah yang paling berkinerja ketika dimaksudkan untuk digunakan sebagai kunci dalam set karena implementasinya Equalsdan GetHashCodemenyebabkan tinju untuk jenis nilai .

Saya akan menggunakan kamus berbasis tupel, tetapi jika saya menginginkan kinerja lebih, saya akan menggunakan tupel saya sendiri dengan implementasi yang lebih baik.


Di samping catatan, beberapa kosmetik dapat membuat kamus keren:

  1. Panggilan bergaya pengindeks bisa jauh lebih bersih dan intuitif. Misalnya,

    string foo = dict[a, b, c]; //lookup
    dict[a, b, c] = ""; //update/insertion

    Jadi tunjukkan pengindeks yang diperlukan dalam kelas kamus Anda yang secara internal menangani penyisipan dan pencarian.

  2. Selain itu, terapkan IEnumerableantarmuka yang sesuai dan berikan Add(TypeA, TypeB, TypeC, string)metode yang akan memberi Anda sintaks penginisialisasi koleksi, seperti:

    new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
    { 
        { a, b, c, null }, 
        ...
    };

Dalam kasus kamus bertingkat, bukankah sintaks pengindeks akan lebih seperti ini string foo = dict[a][b][c]:?
Steven Rands

@ StevenRands ya itu akan.
nawfal

1
@nawfal Dapatkah saya mencari kamus tuple jika saya hanya memiliki satu kunci, tidak semua? atau Dapatkah saya melakukan seperti ini dikt [a, b] lalu dikt [a, c]?
Khan Engineer

@KhanEngineer Banyak dari itu tergantung pada apa tujuan yang dimaksudkan dari kamus atau bagaimana Anda berniat menggunakannya. Misalnya, Anda ingin mendapatkan nilai kembali dengan sebagian kunci a,. Anda bisa saja mengulang kamus apa pun seperti koleksi biasa dan memeriksa properti kunci jika ya a. Jika Anda selalu ingin mendapatkan item di dict by first property maka Anda dapat mendesain kamus dengan lebih baik sebagai kamus kamus seperti yang ditunjukkan dalam jawaban saya dan kueri seperti dict[a], yang memberi Anda kamus lain.
nawfal

Jika dengan "mencari hanya dengan satu kunci" Anda bermaksud untuk mendapatkan nilai kembali dengan salah satu kunci yang Anda miliki, maka Anda sebaiknya mendesain ulang kamus Anda sebagai semacam "kamus kunci apa saja". Misalnya jika Anda ingin mendapatkan nilai 4untuk kedua kunci adan b, maka Anda bisa menjadikannya kamus standar dan menambahkan nilai seperti dict[a] = 4dan dict[b] = 4. Mungkin tidak masuk akal jika secara logis Anda adan bharus menjadi satu unit. Dalam kasus seperti itu, Anda dapat menentukan kustom IEqualityCompareryang menyamakan dua contoh kunci sama jika salah satu propertinya sama. Semua ini secara umum dapat dilakukan dengan refelction.
nawfal

30

Jika Anda menggunakan C # 7, Anda harus mempertimbangkan untuk menggunakan tupel nilai sebagai kunci komposit Anda. Tupel nilai biasanya menawarkan kinerja yang lebih baik daripada tupel referensi tradisional ( Tuple<T1, …>) karena tupel nilai adalah tipe nilai (struct), bukan tipe referensi, sehingga menghindari alokasi memori dan biaya pengumpulan sampah. Selain itu, mereka menawarkan sintaks yang ringkas dan lebih intuitif, memungkinkan bidang mereka diberi nama jika Anda menginginkannya. Mereka juga mengimplementasikan IEquatable<T>antarmuka yang dibutuhkan untuk kamus.

var dict = new Dictionary<(int PersonId, int LocationId, int SubjectId), string>();
dict.Add((3, 6, 9), "ABC");
dict.Add((PersonId: 4, LocationId: 9, SubjectId: 10), "XYZ");
var personIds = dict.Keys.Select(k => k.PersonId).Distinct().ToList();

13

Cara yang baik, bersih, cepat, mudah dan mudah dibaca adalah:

  • menghasilkan metode anggota kesetaraan (Equals () dan GetHashCode ()) untuk tipe saat ini. Alat seperti ReSharper tidak hanya membuat metode, tetapi juga menghasilkan kode yang diperlukan untuk pemeriksaan kesetaraan dan / atau untuk menghitung kode hash. Kode yang dihasilkan akan lebih optimal daripada realisasi Tuple.
  • buat saja kelas kunci sederhana yang diturunkan dari tupel .

tambahkan sesuatu yang mirip seperti ini:

public sealed class myKey : Tuple<TypeA, TypeB, TypeC>
{
    public myKey(TypeA dataA, TypeB dataB, TypeC dataC) : base (dataA, dataB, dataC) { }

    public TypeA DataA => Item1; 

    public TypeB DataB => Item2;

    public TypeC DataC => Item3;
}

Jadi Anda bisa menggunakannya dengan kamus:

var myDictinaryData = new Dictionary<myKey, string>()
{
    {new myKey(1, 2, 3), "data123"},
    {new myKey(4, 5, 6), "data456"},
    {new myKey(7, 8, 9), "data789"}
};
  • Anda juga bisa menggunakannya dalam kontrak
  • sebagai kunci untuk bergabung atau mengelompokkan di LINQ
  • dengan cara ini Anda tidak akan pernah salah ketik urutan Item1, Item2, Item3 ...
  • Anda tidak perlu mengingat atau melihat kode untuk memahami ke mana harus pergi untuk mendapatkan sesuatu
  • tidak perlu mengganti IStructuralEquatable, IStructuralComparable, IComparable, ITuple semuanya sudah ada di sini

1
Sekarang Anda dapat menggunakan ekspresi anggota bertubuh bahkan lebih bersih misalnyapublic TypeA DataA => Item1;
andrewtatham

7

Jika karena alasan tertentu Anda benar-benar ingin menghindari membuat kelas Tuple Anda sendiri, atau menggunakan built-in ke dalam .NET 4.0, ada satu pendekatan lain yang mungkin; Anda dapat menggabungkan tiga nilai kunci menjadi satu nilai.

Misalnya, jika ketiga nilai adalah tipe integer bersama-sama yang tidak mengambil lebih dari 64 bit, Anda dapat menggabungkannya menjadi ulong.

Kasus terburuk Anda selalu dapat menggunakan string, selama Anda memastikan tiga komponen di dalamnya dibatasi dengan beberapa karakter atau urutan yang tidak terjadi di dalam komponen kunci, misalnya, dengan tiga angka yang dapat Anda coba:

string.Format("{0}#{1}#{2}", key1, key2, key3)

Jelas ada beberapa overhead komposisi dalam pendekatan ini, tetapi tergantung pada apa yang Anda gunakan untuk ini mungkin cukup sepele untuk tidak memedulikannya.


6
Saya akan mengatakan bahwa itu sangat bergantung pada konteks; jika saya memiliki tiga tipe integer untuk digabungkan, dan kinerjanya tidak kritis, ini bekerja dengan baik dengan sedikit kemungkinan membuat kesalahan. Tentu saja, semua ini benar-benar mubazir pada .NET 4, karena Microsoft akan memberi kami (mungkin benar!) Jenis Tuple di luar kotak.
jerryjvl

Anda bahkan dapat menggunakan metode ini dalam kombinasi dengan a JavaScriptSerializeruntuk menggabungkan array string dan / atau tipe integer untuk Anda. Dengan cara ini, Anda tidak perlu membuat sendiri karakter pembatas.
binki

3
Hal ini bisa berantakan nyata jika salah satu tombol ( key1, key2, key3) adalah string yang berisi deliminator ( "#")
Greg

4

Saya akan mengganti Tuple Anda dengan GetHashCode yang tepat, dan hanya menggunakannya sebagai kuncinya.

Selama Anda membebani metode yang tepat, Anda akan melihat kinerja yang layak.


1
IComparable tidak berpengaruh pada bagaimana kunci disimpan atau ditempatkan dalam Dictionary <TKey, TValue>. Semuanya dilakukan melalui GetHashCode () dan IEqualityComparer <T>. Menerapkan IEquatable <T> akan mencapai kinerja yang lebih baik karena mengurangi tinju yang disebabkan oleh EqualityComparer default, yang menggunakan fungsi Equals (object).
Dustin Campbell

Saya akan menyebutkan GetHashCode, tetapi saya pikir Kamus menggunakan IComparable dalam hal HashCodes itu Identik ... kira saya salah.
John Gietzen

3

Berikut adalah tupel .NET untuk referensi:

[Serializable] 
public class Tuple<T1, T2, T3> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple {

    private readonly T1 m_Item1; 
    private readonly T2 m_Item2;
    private readonly T3 m_Item3; 

    public T1 Item1 { get { return m_Item1; } }
    public T2 Item2 { get { return m_Item2; } }
    public T3 Item3 { get { return m_Item3; } } 

    public Tuple(T1 item1, T2 item2, T3 item3) { 
        m_Item1 = item1; 
        m_Item2 = item2;
        m_Item3 = item3; 
    }

    public override Boolean Equals(Object obj) {
        return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);; 
    }

    Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) { 
        if (other == null) return false;

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            return false; 
        }

        return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2) && comparer.Equals(m_Item3, objTuple.m_Item3); 
    }

    Int32 IComparable.CompareTo(Object obj) {
        return ((IStructuralComparable) this).CompareTo(obj, Comparer<Object>.Default);
    }

    Int32 IStructuralComparable.CompareTo(Object other, IComparer comparer) {
        if (other == null) return 1; 

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            throw new ArgumentException(Environment.GetResourceString("ArgumentException_TupleIncorrectType", this.GetType().ToString()), "other");
        }

        int c = 0;

        c = comparer.Compare(m_Item1, objTuple.m_Item1); 

        if (c != 0) return c; 

        c = comparer.Compare(m_Item2, objTuple.m_Item2);

        if (c != 0) return c; 

        return comparer.Compare(m_Item3, objTuple.m_Item3); 
    } 

    public override int GetHashCode() { 
        return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
    }

    Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer) { 
        return Tuple.CombineHashCodes(comparer.GetHashCode(m_Item1), comparer.GetHashCode(m_Item2), comparer.GetHashCode(m_Item3));
    } 

    Int32 ITuple.GetHashCode(IEqualityComparer comparer) {
        return ((IStructuralEquatable) this).GetHashCode(comparer); 
    }
    public override string ToString() {
        StringBuilder sb = new StringBuilder();
        sb.Append("("); 
        return ((ITuple)this).ToString(sb);
    } 

    string ITuple.ToString(StringBuilder sb) {
        sb.Append(m_Item1); 
        sb.Append(", ");
        sb.Append(m_Item2);
        sb.Append(", ");
        sb.Append(m_Item3); 
        sb.Append(")");
        return sb.ToString(); 
    } 

    int ITuple.Size { 
        get {
            return 3;
        }
    } 
}

3
Dapatkan kode hash diimplementasikan sebagai ((item1 ^ item2) * 33) ^ item3
Michael Graczyk

2

Jika kode konsumsi Anda dapat dilakukan dengan antarmuka IDictionary <>, alih-alih Kamus, naluri saya akan menggunakan SortedDictionary <> dengan pembanding array kustom, yaitu:

class ArrayComparer<T> : IComparer<IList<T>>
    where T : IComparable<T>
{
    public int Compare(IList<T> x, IList<T> y)
    {
        int compare = 0;
        for (int n = 0; n < x.Count && n < y.Count; ++n)
        {
            compare = x[n].CompareTo(y[n]);
        }
        return compare;
    }
}

Dan buatlah demikian (menggunakan int [] hanya untuk contoh konkret):

var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.