Складений ключовий словник


90

У мене є кілька об’єктів у списку, скажімо, List<MyClass>і MyClass має кілька властивостей. Я хотів би створити індекс списку на основі 3 властивостей MyClass. У цьому випадку 2 властивості - це int, а одна властивість - datetime.

В основному я хотів би мати можливість зробити щось на зразок:

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

Іноді я створюю декілька словників у списку, щоб індексувати різні властивості класів, які він містить. Я не впевнений, як найкраще обробляти композитні клавіші. Я думав зробити контрольну суму для трьох значень, але це ризикує зіткнень.


2
Чому ви не використовуєте Кортежі? Вони роблять для вас усі композиції.
Загадка Eldritch

21
Я не знаю, як на це реагувати. Ви ставите це запитання так, ніби ви припустили, що я навмисно уникаю кортежів.
AaronLS

6
Вибачте, я переписав це як більш детальну відповідь.
Eldritch Conundrum

1
Перш ніж впроваджувати спеціальний клас, прочитайте про Tuple (як запропонував Eldritch Conundrum) - msdn.microsoft.com/en-us/library/system.tuple.aspx . Їх легше змінити, і ви заощадите створення спеціальних класів.
OSH

Відповіді:


105

Вам слід використовувати кортежі. Вони еквівалентні класу CompositeKey, але Equals () та GetHashCode () вже реалізовані для вас.

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")];

Або за допомогою System.Linq

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

Якщо вам не потрібно налаштовувати обчислення хешу, простіше використовувати кортежі.

Якщо є багато властивостей, які ви хочете включити до складеного ключа, ім’я типу Tuple може стати досить довгим, але ви можете скоротити ім’я, створивши власний клас, що походить від Tuple <...>.


** відредаговано в 2017 **

Існує новий варіант, що починається з C # 7: значення кортежів . Ідея однакова, але синтаксис інший, легший:

Тип Tuple<int, bool, string>стає (int, bool, string), а значення Tuple.Create(4, true, "t")стає (4, true, "t").

За допомогою кортежів значень також стає можливим називати елементи. Зверніть увагу, що показники дещо відрізняються, тому, можливо, ви захочете провести порівняльний аналіз, якщо вони для вас важливі.


4
Кортеж не є вдалим кандидатом на ключ, оскільки створює велику кількість колізійних зіткнень. stackoverflow.com/questions/12657348/…
папарацці

1
@Blam KeyValuePair<K,V>та інші структури мають за замовчуванням хеш-функцію, яка, як відомо, є поганою (докладніше див. Stackoverflow.com/questions/3841602/… ). Tuple<>однак не є ValueType, і його хеш-функція за замовчуванням принаймні використовуватиме всі поля. При цьому, якщо основною проблемою вашого коду є зіткнення, тоді впровадьте оптимізовану, GetHashCode()яка підходить для ваших даних.
Eldritch Conundrum

1
Незважаючи на те, що Tuple не є ValueType з мого тестування, він страждає від багатьох зіткнень
paparazzo

5
Я думаю, що ця відповідь застаріла зараз, коли у нас є ValueTuples. Вони мають симпатичніший синтаксис у C #, і вони, здається, роблять GetHashCode вдвічі швидше, ніж Tuples
Lucian Wischik

3
@LucianWischik Дякую, я оновив відповідь, щоб згадати їх.
Eldritch Conundrum

22

Найкращий спосіб, про який я міг подумати, - це створити структуру CompositeKey і переконатись замінити методи GetHashCode () і Equals (), щоб забезпечити швидкість і точність роботи з колекцією:

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;
        }
    }
}

Стаття MSDN про GetHashCode ():

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


Я не думаю, що насправді на 100% впевнений, що це унікальний хеш-код, дуже ймовірно.
Ганс Олссон,

Це цілком може бути правдою! Згідно зі зв’язаною статтею MSDN, це рекомендований спосіб замінити GetHashCode (). Однак, оскільки я не використовую багато складових клавіш у своїй щоденній роботі, я не можу сказати напевно.
Аллен Е. Шарфенберг

4
Так. Якщо розібрати Dictionary.FindEntry () за допомогою Reflector, ви побачите, що перевіряються як хеш-код, так і повна рівність. Хеш-код перевіряється спочатку і, якщо він не вдається, коротко замикає стан, не перевіряючи повну рівність. Якщо хеш проходить, перевіряється рівність.
Джейсон Клебан,

1
І так, рівні також повинні бути замінені на відповідність. Навіть якби ви змусили GetHashCode () повернути 0 для будь-якого примірника, Словник все одно працював би, він був би повільнішим.
Джейсон Клебан,

2
Вбудований тип Tuple реалізує хеш-комбінацію як '(h1 << 5) + h1 ^ h2' замість вашого 'h1 ^ h2'. Я припускаю, що вони роблять це, щоб уникнути зіткнень кожного разу, коли два об'єкти для хешування дорівнюють одному значенню.
Eldritch Conundrum

13

Як щодо Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

Це дозволить вам зробити:

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

1
це створить набагато більше об’єктів, ніж вилучити структуру або клас CompositeKey. а також буде повільнішим, оскільки будуть використовуватися два рівні пошуку.
Ян Рінгроуз,

Я вважаю, що це однакова кількість порівнянь - я не бачу, як було б набагато більше об’єктів - складений ключ все ще потребує ключа, і це значення компонентів або об’єкти та один дикт для їх утримання. Таким вкладеним способом вам не потрібен ключ обгортки для кожного об’єкта / значення, один додатковий дикт для кожного додаткового рівня вкладеності. Що ти думаєш?
Джейсон Клебан,

9
Виходячи з мого тестування, яке я спробував із клавішами з 2 та 3 частин: рішення вкладеного словника в 3-4 рази швидше, ніж використання підходу складеного ключа. Однак підхід з кортежами набагато простіший / акуратніший.
RickL

5
@RickL Я можу підтвердити ці тести, ми використовуємо в нашій кодовій базі тип, який називається CompositeDictionary<TKey1, TKey2, TValue>(і т.д.), який просто успадковується від Dictionary<TKey1, Dictionary<TKey2, TValue>>(або скільки вкладених словників потрібно. Без реалізації всього типу з нуля, ми самі (замість того, щоб обдурити вкладені словники або типи, що містять ключі) це найшвидше, що ми отримуємо.
Адам Гоудсворт,

1
Підхід до вкладеного дикту повинен бути швидшим лише в половині (?) Випадків, коли даних немає, оскільки проміжні словники можуть обійти повне обчислення та порівняння хеш-коду. За наявності даних це повинно бути повільнішим, оскільки основні операції, як Додати, Містить тощо, слід виконувати тричі. Упевнений, що маржа з ​​підходом кортежів побита в деяких згаданих вище тестах щодо деталей реалізації кортежів .NET, що досить погано, враховуючи штраф, який він приносить для типів цінностей. Правильно реалізований триплет - це те, з чим я б пішов, враховуючи також пам’ять
nawfal

12

Ви можете зберігати їх у структурі та використовувати як ключ:

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

Посилання для отримання хеш-коду: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx


Я застряг у .NET 3.5, тому не маю доступу до Tuples, тому це хороше рішення!
aarona

Я здивований, що це не є більш прихильним. Це просте рішення, яке легше читати, ніж Кортеж.
Позначте

1
Відповідно до msdn це виконує нормально, якщо жодне поле не є посилальним типом, інакше для рівності використовується відображення.
Gregor Slavec

@Mark Проблема зі структурою полягає в тому, що її реалізація GetHashCode () за замовчуванням насправді не гарантує використання всіх полів структури (що призводить до поганої роботи словника), тоді як Tuple пропонує таку гарантію. Я це перевірив. Дивіться stackoverflow.com/questions/3841602/… для отримання детальних відомостей.
Елдріч-загадка

8

Тепер, коли вийшов VS2017 / C # 7, найкраща відповідь - використовувати ValueTuple:

// 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)];

Я вирішив оголосити словник анонімним ValueTuple (string, string, int). Але я міг би дати їм імена (string name, string path, int id).

Ідеально, новий ValueTuple швидший, ніж Tuple, GetHashCodeале повільніший Equals. Думаю, вам потрібно буде провести повні наскрізні експерименти, щоб з’ясувати, що насправді найшвидше для вашого сценарію. Але наскрізна приємність та синтаксис мови для ValueTuple змушує його перемагати.

// 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

Так, я пройшов великий перезапис лише для того, щоб рішення "Анонімний тип" підірвало мені обличчя (не можу порівняти анонімні типи, створені з різними збірками). ValueTuple, здається, є відносно елегантним рішенням проблеми складених ключів словника.
Кварклі

5

Відразу ж з’являються два підходи:

  1. Зробіть так, як запропонував Кевін, і напишіть структуру, яка послужить вашим ключем. Не забудьте зробити цю структуру реалізованою IEquatable<TKey>та замінити її Equalsта GetHashCodeметоди *.

  2. Напишіть клас, який внутрішньо використовує вкладені словники. Що - щось на кшталт: TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>... цей клас буде всередині є елемент типу Dictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>, і піддасть методи , такі як this[TKey1 k1, TKey2 k2, TKey3 k3], ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3)і т.д.

* Слово про те, чи потрібно замінювати Equalsметод: хоча це правда, що Equalsметод для структури порівнює значення кожного члена за замовчуванням, він робить це за допомогою відображення - що за своєю суттю тягне за собою витрати на продуктивність - і, отже, не дуже відповідна реалізація для чогось, що має використовуватися як ключ у словнику (як би там не було). Відповідно до документації MSDN про ValueType.Equals:

Реалізація за замовчуванням методу Equals використовує відображення для порівняння відповідних полів obj та цього екземпляра. Перевизначте метод Equals для певного типу, щоб покращити ефективність методу та більш точно представити концепцію рівності для типу.


Що стосується 1, я не думаю, що юо потрібно перевизначити Equals та GetHashcode, реалізація за замовчуванням Equals автоматично перевірятиме рівність у всіх полях, що, на мою думку, має бути добре в цій структурі.
Ганс Олссон,

@ho: Можливо, це не потрібно , але я настійно рекомендую робити це для будь-якої структури, яка буде служити ключем. Перегляньте мою редакцію.
Дан Тао

3

Якщо ключ є частиною класу, тоді використовуйте KeyedCollection.
Це Dictionaryде ключ походить від об’єкта.
Під обкладинками - «Словник».
Не потрібно повторювати клавіші в Keyі Value.
Навіщо ризикувати, ключ не такий, Keyяк у Value.
Не потрібно дублювати одну і ту ж інформацію в пам'яті.

Клас KeyedCollection

Індексатор, щоб виставити складений ключ

    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, то Microsoft спеціально рекомендує його не використовувати.

ValueType.GetHashCode

Tuple технічно не є типом значення, але страждає від одного і того ж симптому (колізійні зіткнення) і не є добрим кандидатом на ключ.


+1 для більш правильної відповіді. Здивований, про це раніше ніхто не згадував. Насправді, залежно від того, як ОП має намір використовувати структуру, HashSet<T>відповідним IEqualityComparer<T>варіантом буде теж варіант. До речі, я думаю, що ваша відповідь залучить голоси, якщо ви зможете змінити імена своїх класів та інші імена членів :)
nawfal

2

Можна запропонувати альтернативу - анонімний об’єкт. Це те саме, що ми використовуємо в методі GroupBy LINQ із кількома клавішами.

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

Це може виглядати дивно, але я порівняв Tuple.GetHashCode та нові методи {a = 1, b = 2} .GetHashCode та анонімні об’єкти на моїй машині в .NET 4.5.1:

Об'єкт - 89,1732 мс для 10000 дзвінків за 1000 циклів

Кортеж - 738,4475 мс для 10000 дзвінків за 1000 циклів


omg, ця альтернатива мені ніколи не спадала на думку ... Я не знаю, чи буде вона поводитися добре, якщо ви використовуєте складний тип як складений ключ.
Габріель Еспіноза

Якщо ви просто передасте об'єкт (замість анонімного), буде використаний результат методу GetHashCode цього об'єкта. Якщо ви використовуєте його так, dictionary[new { a = my_obj, b = 2 }]тоді отриманий хеш-код буде комбінацією my_obj.GetHashCode та ((Int32) 2) .GetHashCode.
Михайло Логутов

НЕ ВИКОРИСТОВУЙТЕ ЦЕЙ МЕТОД! Різні збірки створюють різні імена для анонімних типів. Хоча це здається вам анонімним, за лаштунками створений конкретний клас, і два об'єкти двох різних класів не будуть рівними оператору за замовчуванням.
Кварклі

І як це має значення в цьому випадку?
Michael

0

Іншим рішенням для вже згаданих було б зберегти якийсь список усіх ключів, створених дотепер, і коли генерується новий об'єкт, ви генеруєте його хеш-код (просто як початкову точку), перевірте, чи він вже є у списку, чи є, потім додайте до нього випадкове значення тощо, поки не отримаєте унікальний ключ, потім зберігайте цей ключ у самому об’єкті та у списку та повертайте його як ключ у будь-який час.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.