Bileşik Anahtar Sözlük


91

Diyelim ki Listede bazı nesnelerim var List<MyClass>ve Sınıfımın birkaç özelliği var. MyClass'ın 3 özelliğine dayalı olarak bir liste indeksi oluşturmak istiyorum. Bu durumda, özelliklerden 2'si int'e aittir ve bir özellik bir tarih saattir.

Temel olarak şöyle bir şey yapabilmek isterim:

Dictionary< CompositeKey , MyClass > MyClassListIndex = Dictionary< CompositeKey , MyClass >();
//Populate dictionary with items from the List<MyClass> MyClassList
MyClass aMyClass = Dicitonary[(keyTripletHere)];

Bazen, tuttuğu sınıfların farklı özelliklerini indekslemek için bir listede birden çok sözlük oluşturuyorum. Yine de bileşik anahtarları en iyi nasıl kullanacağımdan emin değilim. Üç değerin bir sağlama toplamını yapmayı düşündüm, ancak bu çarpışma riskini doğuruyor.


2
Neden Tuples kullanmıyorsun? Tüm birleştirme işlemini sizin için yapıyorlar.
Eldritch muamma

21
Buna nasıl cevap vereceğimi bilmiyorum. Bu soruyu, kasıtlı olarak tuple'lardan kaçındığımı varsaymış gibi sorarsınız.
AaronLS

6
Üzgünüm, daha detaylı bir cevap olarak yeniden yazdım.
Eldritch Conundrum

1
Özel bir sınıf uygulamadan önce Tuple hakkında okuyun (Eldritch Conundrum tarafından önerildiği gibi) - msdn.microsoft.com/en-us/library/system.tuple.aspx . Değiştirilmeleri daha kolaydır ve sizi özel sınıflar oluşturmaktan kurtarır.
OSH

Yanıtlar:


108

Tuple kullanmalısınız. Bir CompositeKey sınıfına eşdeğerdirler, ancak Equals () ve GetHashCode () sizin için zaten uygulanmıştır.

var myClassIndex = new Dictionary<Tuple<int, bool, string>, MyClass>();
//Populate dictionary with items from the List<MyClass> MyClassList
foreach (var myObj in myClassList)
    myClassIndex.Add(Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString), myObj);
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

Veya System.Linq kullanarak

var myClassIndex = myClassList.ToDictionary(myObj => Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString));
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

Karmanın hesaplamasını özelleştirmeniz gerekmedikçe, demetleri kullanmak daha kolaydır.

Bileşik anahtara dahil etmek istediğiniz çok sayıda özellik varsa, Tuple türü adı oldukça uzun olabilir, ancak Tuple <...> 'den türetilen kendi sınıfınızı oluşturarak adı daha kısa yapabilirsiniz.


** 2017'de düzenlendi **

C # 7 ile başlayan yeni bir seçenek var: değer tuples . Fikir aynı, ancak sözdizimi farklı, daha hafif:

Tür Tuple<int, bool, string>olur (int, bool, string)ve değer Tuple.Create(4, true, "t")olur (4, true, "t").

Değer dizileriyle öğeleri adlandırmak da mümkün hale gelir. Performansların biraz farklı olduğunu unutmayın, bu nedenle sizin için önemliyse bazı kıyaslamalar yapmak isteyebilirsiniz.


4
Tuple, çok sayıda hash çarpışması yarattığı için bir anahtar için iyi bir aday değildir. stackoverflow.com/questions/12657348/…
paparazzo

1
@Blam KeyValuePair<K,V>ve diğer yapıların, kötü olduğu bilinen varsayılan bir hash işlevi vardır ( daha fazla ayrıntı için stackoverflow.com/questions/3841602/… bakın). Tuple<>ancak bir ValueType değildir ve varsayılan karma işlevi en azından tüm alanları kullanır. Bununla birlikte, kodunuzun ana sorunu çarpışmalarsa, GetHashCode()verilerinize uyan optimize edilmiş bir uygulama yapın.
Eldritch Conundrum

1
Kayıt düzeni benim test bir ValueType olmamasına rağmen bu çarpışmaların aa çok muzdarip
paparazi

5
Artık ValueTuples'a sahip olduğumuz için bu cevabın güncel olmadığını düşünüyorum. C # dilinde
Lucian

3
@LucianWischik Teşekkürler, onlardan bahsetmek için cevabı güncelledim.
Eldritch Conundrum

22

Düşünebildiğim en iyi yol, bir CompositeKey yapısı oluşturmak ve koleksiyonla çalışırken hız ve doğruluk sağlamak için GetHashCode () ve Equals () yöntemlerini geçersiz kıldığından emin olmaktır:

class Program
{
    static void Main(string[] args)
    {
        DateTime firstTimestamp = DateTime.Now;
        DateTime secondTimestamp = firstTimestamp.AddDays(1);

        /* begin composite key dictionary populate */
        Dictionary<CompositeKey, string> compositeKeyDictionary = new Dictionary<CompositeKey, string>();

        CompositeKey compositeKey1 = new CompositeKey();
        compositeKey1.Int1 = 11;
        compositeKey1.Int2 = 304;
        compositeKey1.DateTime = firstTimestamp;

        compositeKeyDictionary[compositeKey1] = "FirstObject";

        CompositeKey compositeKey2 = new CompositeKey();
        compositeKey2.Int1 = 12;
        compositeKey2.Int2 = 9852;
        compositeKey2.DateTime = secondTimestamp;

        compositeKeyDictionary[compositeKey2] = "SecondObject";
        /* end composite key dictionary populate */

        /* begin composite key dictionary lookup */
        CompositeKey compositeKeyLookup1 = new CompositeKey();
        compositeKeyLookup1.Int1 = 11;
        compositeKeyLookup1.Int2 = 304;
        compositeKeyLookup1.DateTime = firstTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup1]);

        CompositeKey compositeKeyLookup2 = new CompositeKey();
        compositeKeyLookup2.Int1 = 12;
        compositeKeyLookup2.Int2 = 9852;
        compositeKeyLookup2.DateTime = secondTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup2]);
        /* end composite key dictionary lookup */
    }

    struct CompositeKey
    {
        public int Int1 { get; set; }
        public int Int2 { get; set; }
        public DateTime DateTime { get; set; }

        public override int GetHashCode()
        {
            return Int1.GetHashCode() ^ Int2.GetHashCode() ^ DateTime.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj is CompositeKey)
            {
                CompositeKey compositeKey = (CompositeKey)obj;

                return ((this.Int1 == compositeKey.Int1) &&
                        (this.Int2 == compositeKey.Int2) &&
                        (this.DateTime == compositeKey.DateTime));
            }

            return false;
        }
    }
}

GetHashCode () ile ilgili bir MSDN makalesi:

http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx


Bunun benzersiz bir hashcode olacağından% 100 emin değilim, büyük olasılıkla.
Hans Olsson

Bu çok iyi olabilir! Bağlantılı MSDN makalesine göre, GetHashCode () 'u geçersiz kılmanın önerilen yolu budur. Ancak, günlük işlerimde çok fazla bileşik anahtar kullanmadığım için kesin olarak söyleyemem.
Allen E. Scharfenberg

4
Evet. Dictionary.FindEntry () öğesini Reflector ile parçalara ayırırsanız, hem hashcode hem de tam eşitliğin test edildiğini görürsünüz. Önce hashcode test edilir ve başarısız olursa, tam eşitliği kontrol etmeden koşulu kısa devre yapar. Hash geçerse eşitlik de test edilir.
Jason Kleban

1
Ve evet, eşitler de eşleşmek için geçersiz kılınmalıdır. GetHashCode () 'un herhangi bir örnek için 0 döndürmesini sağlamış olsanız bile Sözlük yine de çalışır, sadece daha yavaş olacaktır.
Jason Kleban

2
Yerleşik Tuple türü, 'h1 ^ h2' yerine '(h1 << 5) + h1 ^ h2' olarak hash kombinasyonunu uygular. Sanırım bunu, karma işlemi yapılacak iki nesne aynı değere eşit olduğunda çarpışmaları önlemek için yapıyorlar.
Eldritch Conundrum

13

Nasıl olur Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

Bu, şunları yapmanıza izin verir:

MyClass item = MyData[8][23923][date];

1
bu bir CompositeKey yapısı veya sınıfı kullanmaktan çok daha fazla nesne yaratacaktır. ve ayrıca iki düzey arama kullanılacağından daha yavaş olacaktır.
Ian Ringrose

Aynı sayıda karşılaştırma olduğuna inanıyorum - nasıl daha fazla nesne olacağını anlamıyorum - bileşik anahtar yolu hala bir anahtara ihtiyaç duyuyor ve bileşen değerleri veya nesneleri ve bunları tutmak için bir emir. Bu iç içe geçmiş şekilde, her nesne / değer için sarmalayıcı anahtarına ihtiyacınız yoktur, her ek yuva düzeyi için ek bir dikteye ihtiyacınız vardır. Ne düşünüyorsun?
Jason Kleban

9
2 ve 3 parçalı anahtarlarla denediğim karşılaştırmama dayanarak: iç içe geçmiş sözlük çözümü, bir tuple bileşik anahtar yaklaşımı kullanmaktan 3-4 kat daha hızlıdır. Ancak, tuple yaklaşımı çok daha kolay / daha derli toplu.
RickL

5
@RickL Bu kriterleri onaylayabilirim, kod tabanımızda CompositeDictionary<TKey1, TKey2, TValue>(vb) adında bir tür kullanıyoruz, bu tür (vb Dictionary<TKey1, Dictionary<TKey2, TValue>>. iç içe geçmiş sözlükler veya anahtarları içeren türler) bu en hızlı aldığımız
şeydir

1
Ara sözlükler tam karma kod hesaplamasını ve karşılaştırmasını atlayabildiğinden, iç içe geçmiş dikte yaklaşımı, verilerin olmadığı durumlarda yalnızca yarısı (?) İçin daha hızlı olmalıdır. Veri varlığında Ekle, İçerir vb. Temel işlemler üç defa yapılması gerektiğinden daha yavaş olmalıdır. Eminim yukarıda bahsedilen bazı kriterlerde tuple yaklaşımı ile marj, değer türleri için getirdiği boks cezası dikkate alındığında oldukça zayıf olan .NET tuples uygulama detaylarıyla ilgilidir. Doğru şekilde uygulanmış bir üçlü, hafızayı da göz önünde bulundurarak benim gideceğim
şeydir

12

Bunları bir yapıda saklayabilir ve bunu anahtar olarak kullanabilirsiniz:

struct CompositeKey
{
  public int value1;
  public int value2;
  public DateTime value3;
}

Karma kodu almak için bağlantı: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx


.NET 3.5'e takılı kaldım, Tuplebu yüzden e- postalara erişimim yok, bu yüzden bu iyi bir çözüm!
aarona

Bunun daha fazla oylanmamasına şaşırdım. Bir Tuple'dan daha okunaklı olan basit bir çözüm.
Mark

1
Msdn'ye göre bu, eğer hiçbir alan referans türü değilse iyi çalışır, aksi takdirde eşitlik için yansıma kullanır.
Gregor Slavec

@Mark Bir yapıdaki sorun, varsayılan GetHashCode () uygulamasının aslında yapının tüm alanlarını kullanmayı garanti etmemesidir (zayıf sözlük performansına yol açar), oysa Tuple böyle bir garanti sunar. Ben test ettim. Bkz stackoverflow.com/questions/3841602/... kanlı detaylar için.
Eldritch muamma

8

Şimdi VS2017 / C # 7 çıktı, en iyi cevap ValueTuple kullanmaktır:

// declare:
Dictionary<(string, string, int), MyClass> index;

// populate:
foreach (var m in myClassList) {
  index[(m.Name, m.Path, m.JobId)] = m;
}

// retrieve:
var aMyClass = index[("foo", "bar", 15)];

Sözlüğü anonim bir ValueTuple ile bildirmeyi seçtim (string, string, int). Ama onlara isimler verebilirdim(string name, string path, int id) .

Mükemmel şekilde, yeni ValueTuple, Tuple'dan daha hızlı GetHashCodeancak daha yavaştır Equals. Senaryonuz için hangisinin gerçekten en hızlı olduğunu bulmak için uçtan uca eksiksiz deneyler yapmanız gerektiğini düşünüyorum. Ancak ValueTuple'ın uçtan uca güzelliği ve dil sözdizimi onu kazançlı kılıyor.

// Perf from https://gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
//
//              Tuple ValueTuple KeyValuePair
//  Allocation:  160   100        110
//    Argument:   75    80         80    
//      Return:   75   210        210
//        Load:  160   170        320
// GetHashCode:  820   420       2700
//      Equals:  280   470       6800

Evet, Anonim Tip çözümünün yüzüme patlaması için büyük bir yeniden yazımdan geçtim (farklı montajlarla oluşturulan anonim türleri karşılaştıramıyorum). ValueTuple, bileşik sözlük anahtarları sorununa nispeten zarif bir çözüm gibi görünüyor.
Quarkly

5

Hemen akla iki yaklaşım gelir:

  1. Kevin'in önerdiği gibi yapın ve anahtarınız olarak hizmet edecek bir yapı yazın. Bu yapının uygulandığından IEquatable<TKey>ve onun Equalsve GetHashCodeyöntemlerini * geçersiz kıldığından emin olun .

  2. İç içe geçmiş sözlükleri dahili olarak kullanan bir sınıf yazın. Bir şey gibi: TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>... Bu sınıf içten tip bir üyesi olurdu Dictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>ve böyle gibi yöntemleri açığa çıkaracağını this[TKey1 k1, TKey2 k2, TKey3 k3], ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3)vb

* EqualsYöntemin geçersiz kılınmasının gerekli olup olmadığına dair bir kelime : EqualsBir yapının yönteminin varsayılan olarak her bir üyenin değerini karşılaştırdığı doğru olsa da, bunu yansıma kullanarak yapar - bu da doğal olarak performans maliyetlerini gerektirir - ve bu nedenle çok fazla değildir. sözlükte anahtar olarak kullanılması amaçlanan bir şey için uygun uygulama (benim görüşüme göre, yine de). Aşağıdaki MSDN belgelerine göre ValueType.Equals:

Equals yönteminin varsayılan uygulaması, ilgili obj alanlarını ve bu örneği karşılaştırmak için yansıma kullanır. Yöntemin performansını iyileştirmek ve tür için eşitlik kavramını daha yakından temsil etmek için belirli bir tür için Eşittir yöntemini geçersiz kılın.


1 ile ilgili olarak, Equals ve GetHashcode'u geçersiz kılmanız gerektiğini düşünmüyorum, Equals'ın varsayılan uygulaması, bu yapıda uygun olduğunu düşündüğüm tüm alanlarda eşitliği otomatik olarak kontrol edecektir.
Hans Olsson

@ho: O olmayabilir gerekli ama şiddetle bir anahtar olarak hizmet edecek herhangi bir yapı için bunu yaparken tavsiye ediyorum. Düzenlememe bakın.
Dan Tao

3

Anahtar sınıfın bir parçasıysa, kullanın KeyedCollection.
Bu ise Dictionaryönemli bir nesne elde edilir.
O Sözlük olduğunu kapakları altında
Do not anahtarı tekrarlamak zorunda Keyve Value.
Neden anahtar aynı olmayan bir şans Keyolarak Value.
Aynı bilgileri bellekte kopyalamak zorunda değilsiniz.

KeyedCollection Sınıfı

Bileşik anahtarı göstermek için dizin oluşturucu

    using System.Collections.ObjectModel;

    namespace IntIntKeyedCollection
    {
        class Program
        {
            static void Main(string[] args)
            {
                Int32Int32DateO iid1 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                Int32Int32DateO iid2 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                if (iid1 == iid2) Console.WriteLine("same");
                if (iid1.Equals(iid2)) Console.WriteLine("equals");
                // that are equal but not the same I don't override = so I have both features

                Int32Int32DateCollection int32Int32DateCollection = new Int32Int32DateCollection();
                // dont't have to repeat the key like Dictionary
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 0, new DateTime(2008, 5, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(iid1);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(iid2);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                Console.WriteLine("count");
                Console.WriteLine(int32Int32DateCollection.Count.ToString());
                // reference by ordinal postion (note the is not the long key)
                Console.WriteLine("oridinal");
                Console.WriteLine(int32Int32DateCollection[0].GetHashCode().ToString());
                // reference by index
                Console.WriteLine("index");
                Console.WriteLine(int32Int32DateCollection[0, 1, new DateTime(2008, 6, 1, 8, 30, 52)].GetHashCode().ToString());
                Console.WriteLine("foreach");
                foreach (Int32Int32DateO iio in int32Int32DateCollection)
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.WriteLine("sorted by date");
                foreach (Int32Int32DateO iio in int32Int32DateCollection.OrderBy(x => x.Date1).ThenBy(x => x.Int1).ThenBy(x => x.Int2))
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.ReadLine();
            }
            public class Int32Int32DateCollection : KeyedCollection<Int32Int32DateS, Int32Int32DateO>
            {
                // This parameterless constructor calls the base class constructor 
                // that specifies a dictionary threshold of 0, so that the internal 
                // dictionary is created as soon as an item is added to the  
                // collection. 
                // 
                public Int32Int32DateCollection() : base(null, 0) { }

                // This is the only method that absolutely must be overridden, 
                // because without it the KeyedCollection cannot extract the 
                // keys from the items.  
                // 
                protected override Int32Int32DateS GetKeyForItem(Int32Int32DateO item)
                {
                    // In this example, the key is the part number. 
                    return item.Int32Int32Date;
                }

                //  indexer 
                public Int32Int32DateO this[Int32 Int1, Int32 Int2, DateTime Date1]
                {
                    get { return this[new Int32Int32DateS(Int1, Int2, Date1)]; }
                }
            }

            public struct Int32Int32DateS
            {   // required as KeyCollection Key must be a single item
                // but you don't really need to interact with Int32Int32DateS directly
                public readonly Int32 Int1, Int2;
                public readonly DateTime Date1;
                public Int32Int32DateS(Int32 int1, Int32 int2, DateTime date1)
                { this.Int1 = int1; this.Int2 = int2; this.Date1 = date1; }
            }
            public class Int32Int32DateO : Object
            {
                // implement other properties
                public Int32Int32DateS Int32Int32Date { get; private set; }
                public Int32 Int1 { get { return Int32Int32Date.Int1; } }
                public Int32 Int2 { get { return Int32Int32Date.Int2; } }
                public DateTime Date1 { get { return Int32Int32Date.Date1; } }

                public override bool Equals(Object obj)
                {
                    //Check for null and compare run-time types.
                    if (obj == null || !(obj is Int32Int32DateO)) return false;
                    Int32Int32DateO item = (Int32Int32DateO)obj;
                    return (this.Int32Int32Date.Int1 == item.Int32Int32Date.Int1 &&
                            this.Int32Int32Date.Int2 == item.Int32Int32Date.Int2 &&
                            this.Int32Int32Date.Date1 == item.Int32Int32Date.Date1);
                }
                public override int GetHashCode()
                {
                    return (((Int64)Int32Int32Date.Int1 << 32) + Int32Int32Date.Int2).GetHashCode() ^ Int32Int32Date.GetHashCode();
                }
                public Int32Int32DateO(Int32 Int1, Int32 Int2, DateTime Date1)
                {
                    Int32Int32DateS int32Int32Date = new Int32Int32DateS(Int1, Int2, Date1);
                    this.Int32Int32Date = int32Int32Date;
                }
            }
        }
    }

Fpr değer türünü kullanmaya gelince, Microsoft özellikle buna karşı tavsiye eder.

ValueType.GetHashCode

Tuple teknik olarak bir değer türü değildir, ancak aynı semptomdan muzdariptir (karma çarpışmalar) ve bir anahtar için iyi bir aday değildir.


Daha doğru bir cevap için +1. Daha önce kimsenin bahsetmediğine şaşırdım. Aslında OP'nin yapıyı nasıl kullanmak istediğine bağlı olarak, HashSet<T>uygun IEqualityComparer<T>bir seçenekle de bir seçenek olacaktır. Btw, sınıf adlarınızı ve diğer üye adlarınızı değiştirebilirseniz cevabınızın oyları artıracağını düşünüyorum :)
nawfal

2

Bir alternatif önerebilir miyim - anonim bir nesne. GroupBy LINQ yönteminde birden çok anahtarla kullandığımızın aynısı.

var dictionary = new Dictionary<object, string> ();
dictionary[new { a = 1, b = 2 }] = "value";

Garip görünebilir, ancak Tuple.GetHashCode'u karşılaştırdım ve yeni {a = 1, b = 2} .GetHashCode yöntemleri ve anonim nesneler makinemde .NET 4.5.1 üzerinde kazanıyor:

Nesne - 89,1732 ms, 1000 döngüde 10000 çağrı için

Tuple - 1000 döngüde 10000 çağrı için 738,4475 ms


Aman Tanrım, bu alternatif asla aklımda değildi ... Kompozit anahtar olarak karmaşık bir tip kullanırsanız iyi davranıp davranmayacağını bilmiyorum.
Gabriel Espinoza

Sadece bir nesneyi (anonim yerine) iletirseniz, bu nesnenin GetHashCode yönteminin sonucu kullanılacaktır. Bunu gibi kullanırsanız dictionary[new { a = my_obj, b = 2 }], sonuçta ortaya çıkan karma kodu my_obj.GetHashCode ve ((Int32) 2) .GetHashCode kombinasyonudur.
Michael Logutov

BU YÖNTEMİ KULLANMAYIN! Farklı derlemeler, anonim tipler için farklı adlar oluşturur. Size anonim görünse de, perde arkasında somut bir sınıf oluşturulmuş ve iki farklı sınıftan iki nesne varsayılan operatörle eşit olmayacaktır.
Quarkly

Ve bu durumda bunun önemi nedir?
Michael Logutov

0

Daha önce bahsedilenlere başka bir çözüm, şimdiye kadar üretilen tüm anahtarların bir tür listesini saklamaktır ve yeni bir nesne oluşturulduğunda, onun karma kodunu oluşturursunuz (sadece bir başlangıç ​​noktası olarak), zaten listede olup olmadığını kontrol edin. daha sonra benzersiz bir anahtar elde edene kadar ona rastgele bir değer vb. ekleyin, ardından bu anahtarı nesnenin kendisinde ve listede saklayın ve bunu her zaman anahtar olarak geri getirin.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.