Составной ключевой словарь

91

У меня есть некоторые объекты в списке, скажем, 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)];

Иногда я создаю несколько словарей в списке для индексации различных свойств содержащихся в нем классов. Однако я не уверен, как лучше всего обрабатывать составные ключи. Я подумал о том, чтобы вычислить контрольную сумму трех значений, но это чревато конфликтами.

AaronLS
источник
2
Почему вы не используете кортежи? Они делают все за вас.
Eldritch Conundrum
21
Я не знаю, как на это ответить. Вы задаете этот вопрос, как будто предполагаете, что я намеренно избегаю кортежей.
AaronLS
6
Извините, переписал как более развернутый ответ.
Eldritch Conundrum
1
Перед реализацией настраиваемого класса прочтите о Tuple (как предлагает Eldritch Conundrum) - msdn.microsoft.com/en-us/library/system.tuple.aspx . Их легче изменить, и они избавят вас от создания пользовательских классов.
OSH

Ответы:

108

Вы должны использовать кортежи. Они эквивалентны классу 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/…
paparazzo
1
@Blam KeyValuePair<K,V>и другие структуры имеют хеш-функцию по умолчанию, которая, как известно, плохая (подробнее см. Stackoverflow.com/questions/3841602/… ). Tuple<>однако это не ValueType, и его хеш-функция по умолчанию, по крайней мере, будет использовать все поля. При этом, если основная проблема вашего кода - это коллизии, тогда реализуйте оптимизированный вариант GetHashCode(), соответствующий вашим данным.
Eldritch Conundrum
1
Несмотря на то, что Tuple не является ValueType из моего тестирования, он страдает от множества коллизий
папараццо
5
Я думаю, что теперь, когда у нас есть ValueTuples, этот ответ устарел. У них более приятный синтаксис на C #, и они, кажется, выполняют GetHashCode в два раза быстрее, чем кортежи - gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
Люсьен Вишик
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 для любого экземпляра, Dictionary все равно будет работать, только медленнее.
Джейсон Клебан
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. и также будет медленнее, так как будет использоваться два уровня поиска.
Ян Рингроуз,
Я считаю, что это такое же количество сравнений - я не понимаю, как могло бы быть намного больше объектов - для составного ключа по-прежнему нужен ключ, и это значения компонентов или объекты и один dict для их хранения. При таком вложенном способе вам не нужен ключ-оболочка для каждого объекта / значения, один дополнительный dict для каждого дополнительного уровня вложенности. Что вы думаете?
Джейсон Клебан,
9
Основываясь на моем тестировании, которое я пробовал с ключами с 2 и 3 частями: решение с вложенным словарем в 3-4 раза быстрее, чем при использовании подхода с составным кортежем. Однако кортежный подход намного проще / аккуратнее.
RickL
5
@RickL Я могу подтвердить эти тесты, мы используем тип в нашей кодовой базе, называемый CompositeDictionary<TKey1, TKey2, TValue>(и т. Д.), Который просто наследуется от Dictionary<TKey1, Dictionary<TKey2, TValue>>(или сколько бы там ни было вложенных словарей). Без реализации всего типа с нуля сами (вместо обмана с использованием вложенные словари или типы, содержащие ключи) это самое быстрое, что мы можем получить.
Адам Хоулдсворт,
1
Подход с вложенным dict должен быть быстрее только в половине (?) Случаев, когда данные отсутствуют, поскольку промежуточные словари могут обойти вычисление и сравнение полного хэш-кода. При наличии данных это должно быть медленнее, поскольку базовые операции, такие как «Добавить», «Содержит» и т. Д., Должны выполняться трижды. Я уверен, что маржа с подходом кортежей побита в некоторых из упомянутых выше тестов, касающихся деталей реализации кортежей .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

kemiller2002
источник
Я застрял на .NET 3.5, поэтому у меня нет доступа к Tuples, так что это хорошее решение!
aarona
Я удивлен, что за это не проголосовали больше. Это простое решение, более читаемое, чем кортеж.
Марк
1
Согласно msdn, это работает нормально, если поля не являются ссылочными типами, в противном случае для равенства используется отражение.
Грегор Славек
@Mark Проблема со структурой заключается в том, что ее реализация по умолчанию GetHashCode () на самом деле не гарантирует использования всех полей структуры (что приводит к низкой производительности словаря), тогда как Tuple предлагает такую ​​гарантию. Я это проверил. См. Stackoverflow.com/questions/3841602/… для подробностей.
Eldritch Conundrum
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
Люсьен Вишик
источник
Да, я прошел через большую переписывание только для того, чтобы решение Anonymous Type взорвалось мне в лицо (не могу сравнивать анонимные типы, созданные с помощью разных сборок). ValueTuple кажется относительно элегантным решением проблемы составных ключей словаря.
Quarkly, 03
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 за более правильный ответ. Удивлен, что об этом раньше никто не упоминал. Фактически, в зависимости от того, как OP намеревается использовать структуру, 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 циклов

Михаил Логутов
источник
Боже мой, я никогда не думал об этой альтернативе ... Я не знаю, будет ли она хорошо себя вести, если вы используете сложный тип в качестве составного ключа.
Габриэль Эспиноза
Если вы просто передадите объект (вместо анонимного), будет использован результат метода GetHashCode этого объекта. Если вы используете его так, dictionary[new { a = my_obj, b = 2 }]то полученный хэш-код будет комбинацией my_obj.GetHashCode и ((Int32) 2) .GetHashCode.
Михаил Логутов
НЕ ИСПОЛЬЗУЙТЕ ЭТОТ МЕТОД! Различные сборки создают разные имена для анонимных типов. Хотя вам это кажется анонимным, за кулисами создается конкретный класс, и два объекта двух разных классов не будут соответствовать оператору по умолчанию.
Quarkly, 03
И какое это имеет значение в данном случае?
Михаил Логутов
0

Другим решением для уже упомянутых было бы сохранить какой-то список всех ключей, сгенерированных на данный момент, и когда создается новый объект, вы генерируете его хэш-код (просто в качестве отправной точки), проверьте, есть ли он уже в списке, если он есть, затем добавьте к нему какое-то случайное значение и т. д., пока не получите уникальный ключ, затем сохраните этот ключ в самом объекте и в списке и всегда возвращайте его как ключ.

Ханс Ольссон
источник