У меня есть некоторые объекты в списке, скажем, 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)];
Иногда я создаю несколько словарей в списке для индексации различных свойств содержащихся в нем классов. Однако я не уверен, как лучше всего обрабатывать составные ключи. Я подумал о том, чтобы вычислить контрольную сумму трех значений, но это чревато конфликтами.
c#
dictionary
AaronLS
источник
источник
Ответы:
Вы должны использовать кортежи. Они эквивалентны классу 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")
.С помощью кортежей значений также становится возможным давать имена элементам. Обратите внимание, что производительность немного отличается, поэтому вы можете провести сравнительный анализ, если он важен для вас.
источник
KeyValuePair<K,V>
и другие структуры имеют хеш-функцию по умолчанию, которая, как известно, плохая (подробнее см. Stackoverflow.com/questions/3841602/… ).Tuple<>
однако это не ValueType, и его хеш-функция по умолчанию, по крайней мере, будет использовать все поля. При этом, если основная проблема вашего кода - это коллизии, тогда реализуйте оптимизированный вариантGetHashCode()
, соответствующий вашим данным.Лучший способ, который я мог придумать, - это создать структуру 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
источник
Как насчет
Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>
?Это позволит вам:
MyClass item = MyData[8][23923][date];
источник
CompositeDictionary<TKey1, TKey2, TValue>
(и т. Д.), Который просто наследуется отDictionary<TKey1, Dictionary<TKey2, TValue>>
(или сколько бы там ни было вложенных словарей). Без реализации всего типа с нуля сами (вместо обмана с использованием вложенные словари или типы, содержащие ключи) это самое быстрое, что мы можем получить.Вы можете сохранить их в структуре и использовать как ключ:
struct CompositeKey { public int value1; public int value2; public DateTime value3; }
Ссылка для получения хэш-кода: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx
источник
Tuple
s, так что это хорошее решение!Теперь, когда вышел 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
источник
На ум сразу приходят два подхода:
Сделайте то, что предложил Кевин, и напишите структуру, которая будет служить вашим ключом. Не забудьте сделать это структура реализации
IEquatable<TKey>
и переопределить егоEquals
иGetHashCode
методы *.Напишите класс, который внутренне использует вложенные словари. Что-то вроде:
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
метод для структуры сравнивает значение каждого члена по умолчанию, он делает это с помощью отражения, что по своей сути влечет за собой затраты на производительность, и, следовательно, не очень подходящая реализация для чего-то, что предназначено для использования в качестве ключа в словаре (в любом случае, на мой взгляд). Согласно документации MSDNValueType.Equals
:источник
Если ключ является частью класса, используйте
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
технически не является типом значения, но страдает тем же симптомом (конфликты хешей) и не подходит для ключа.источник
HashSet<T>
подходящийIEqualityComparer<T>
вариант тоже будет. Кстати, я думаю, что ваш ответ привлечет больше голосов, если вы сможете изменить имена своих классов и других участников :)Могу предложить альтернативу - анонимный объект. То же самое мы используем в методе 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 циклов
источник
dictionary[new { a = my_obj, b = 2 }]
то полученный хэш-код будет комбинацией my_obj.GetHashCode и ((Int32) 2) .GetHashCode.Другим решением для уже упомянутых было бы сохранить какой-то список всех ключей, сгенерированных на данный момент, и когда создается новый объект, вы генерируете его хэш-код (просто в качестве отправной точки), проверьте, есть ли он уже в списке, если он есть, затем добавьте к нему какое-то случайное значение и т. д., пока не получите уникальный ключ, затем сохраните этот ключ в самом объекте и в списке и всегда возвращайте его как ключ.
источник