Словарь композитных ключевых слов
У меня есть некоторые объекты в List, скажем 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)];
Я иногда создаю несколько словарей в списке, чтобы индексировать различные свойства классов, которые он держит. Я не уверен, как лучше всего работать с составными ключами. Я считал выполнение контрольной суммы трех значений, но это рискует столкновениями.
Ответы
Ответ 1
Вы должны использовать кортежи. Они эквивалентны классу 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 году **
Существует новый параметр, начинающийся с С# 7: кортежи значений. Идея такая же, но синтаксис другой, легче:
Тип Tuple<int, bool, string>
становится (int, bool, string)
, а значение Tuple.Create(4, true, "t")
становится (4, true, "t")
.
Со значениями кортежей также становится возможным называть элементы. Обратите внимание, что производительность немного отличается, поэтому вы можете захотеть провести сравнительный анализ, если они важны для вас.
Ответ 2
Лучший способ, которым я мог подумать, - создать структуру 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
Ответ 3
Как насчет Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>
?
Это позволит вам сделать:
MyClass item = MyData[8][23923][date];
Ответ 4
Вы можете сохранить их в структуре и использовать в качестве ключа:
struct CompositeKey
{
public int value1;
public int value2;
public DateTime value3;
}
Ссылка для получения хэш-кода:
http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx
Ответ 5
Два подхода сразу spring:
-
Сделайте, как предложил Кевин, и напишите структуру, которая будет служить вашим ключом. Обязательно создайте этот конструктор 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
: хотя true, что метод Equals
для структуры сравнивает значение каждого элемента по умолчанию, он делает это с помощью отражения, которое по своей сути влечет за собой производительность затраты - и, следовательно, не очень подходящая реализация для чего-то, предназначенного для использования в качестве ключа в словаре (по-моему, во всяком случае). Согласно документации MSDN на ValueType.Equals
:
Стандартная реализация Метод Equals использует отражение для сравните соответствующие поля obj и этот экземпляр. Переопределить Метод Equals для определенного типа повысить эффективность метода и более точно представляют концепцию равенства для типа.
Ответ 6
Если ключ является частью класса, используйте KeyedCollection.
Это словарь, в котором ключ получен из объекта.
Под обложками находится Словарь
Не нужно повторять ключ в ключе и значении.
Зачем рисковать ключом не является ключом в качестве значения.
Не нужно дублировать одну и ту же информацию в памяти.
Класс 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 технически не является типом значений, но страдает от одного и того же симптома (хеш-коллизии) и не является хорошим кандидатом для ключа.
Ответ 7
Могу ли я предложить альтернативу - анонимный объект. То же самое мы используем в методе 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 циклов
Tuple - 738,4475 мс для 10000 вызовов в 1000 циклов
Ответ 8
Теперь, когда VS2017/С# 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
Ответ 9
Другим решением для уже упомянутых было бы сохранить какой-то список всех ключей, сгенерированных до сих пор, и когда создается новый объект, вы генерируете его hashcode (как отправную точку), проверьте, уже ли он в списке, если это так, добавьте к нему некоторое случайное значение и до тех пор, пока у вас не будет уникального ключа, а затем сохраните этот ключ в самом объекте и в списке и верните его как ключ во все времена.