复合键字典


90

我在列表中有一些对象,比方说 List<MyClass>,MyClass具有几个属性。我想基于MyClass的3个属性创建列表的索引。在这种情况下,其中两个属性是int的,一个属性是日期时间。

基本上,我希望能够执行以下操作:

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

有时,我会在列表上创建多个词典,以为其包含的类的不同属性建立索引。我不确定如何最好地处理复合键。我考虑对这三个值进行校验和,但这会带来冲突的风险。


2
你为什么不使用元组?他们为您完成所有合成工作。
Eldritch难题,2012年

21
我不知道该如何回应。您问这个问题,就好像您已经假设我在故意避免使用元组一样。
AaronLS

6
抱歉,我将其重写为更详细的答案。
Eldritch难题,2012年

1
在实现自定义类之前,请阅读有关Tuple的信息(如Eldritch Conundrum所建议)-msdn.microsoft.com/zh-cn/library/system.tuple.aspx。它们更容易更改,并且可以节省创建自定义类的时间。
OSH 2012年

Answers:


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开始有一个新选项:value tuples。想法是一样的,但是语法不同,但更轻松:

类型Tuple<int, bool, string>变为(int, bool, string),值Tuple.Create(4, true, "t")变为(4, true, "t")

使用值元组,也可以命名元素。请注意,性能略有不同,因此,如果它们对您很重要,则可能需要进行一些基准测试。


4
元组不是密钥的理想选择,因为它会产生大量的哈希冲突。 stackoverflow.com/questions/12657348/...
狗仔队

1
@BlamKeyValuePair<K,V>和其他结构具有默认的哈希函数,该函数被认为是不好的(有关更多详细信息,请参见stackoverflow.com/questions/3841602/…)。Tuple<>但是不是ValueType,它的默认哈希函数至少会使用所有字段。话虽这么说,如果代码的主要问题是冲突,那么请实施GetHashCode()适合您数据的优化。
Eldritch难题,2014年

1
即使Tuple不是我测试中的ValueType,它也会遭受很多碰撞
狗仔队2014年

5
我认为现在有了ValueTuples,这个答案已经过时了。他们在C#中具有更好的语法,并且它们执行GetHashCode的速度是元组的两倍-gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
Lucian Wischik

3
@LucianWischik谢谢,我已经更新了答案以提及他们。
Eldritch难题,2017年

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

关于GetHashCode()的MSDN文章:

http://msdn.microsoft.com/zh-CN/library/system.object.gethashcode.aspx


我认为实际上不可能100%确定是唯一的哈希码。
汉斯·奥尔森,2010年

那可能是真的!根据链接的MSDN文章,这是重写GetHashCode()的推荐方法。但是,由于我在日常工作中没有使用很多复合键,因此无法确定。
艾伦·沙芬伯格

4
是。如果使用Reflector分解Dictionary.FindEntry(),您将看到哈希码和完全相等性都经过测试。首先测试哈希码,如果哈希码失败,则在不检查完全相等的情况下使条件短路。如果哈希通过,则也将测试相等性。
杰森·克莱本

1
是的,equals也应该被覆盖以匹配。即使您使GetHashCode()对于任何实例都返回0,Dictionary仍然可以工作,只是速度较慢。
杰森·克莱本

2
内置的Tuple类型将哈希组合实现为'(h1 << 5)+ h1 ^ h2',而不是'h1 ^ h2'。我想他们这样做是为了避免每次要哈希的两个对象等于相同值时发生冲突。
Eldritch难题,2012年

13

怎么Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>

这将允许您执行以下操作:

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

1
这将创建更多对象,然后使用CompositeKey结构或类。并且也会变慢,因为将使用两个级别的查找。
伊恩·林罗斯

我相信比较的次数是相同的-我看不到会有更多的对象-复合键方式仍然需要一个键,它是组件值或对象以及一个保存它们的字典。通过这种嵌套方式,您不需要每个对象/值的包装键,也不需要每个附加嵌套级别的附加字典。你怎么看?
杰森·克莱本

9
根据我的基准测试,我尝试使用具有2个和3个部分的键:嵌套字典解决方案比使用元组复合键方法快3-4倍。但是,元组方法要容易/整齐。
RickL 2012年

5
@RickL我可以确认那些基准,我们在代码库中使用了一个类型,称为CompositeDictionary<TKey1, TKey2, TValue>(etc),该类型仅继承自Dictionary<TKey1, Dictionary<TKey2, TValue>>(或者需要许多嵌套词典。不需要自己从头开始实现整个类型(而不是使用嵌套的字典或包含密钥的类型),这是我们获得的最快的速度
Adam Houldsworth,2012年

1
嵌套dict方法仅在不存在数据的情况下才快一半(?),因为中间词典可以绕过完整的哈希码计算和比较。在有数据的情况下,它应该变慢,因为应执行三次“添加”,“包含”等基本操作。我肯定在上面提到的一些基准测试中,元组方法的利润超出了.NET元组的实现细节,考虑到它给值类型带来的损失,这是非常差的。如果考虑到内存,我会采用正确实现的三元组
nawfal 2014年

12

您可以将它们存储在结构中并将其用作键:

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

链接以获取哈希码:http : //msdn.microsoft.com/zh-cn/library/system.valuetype.gethashcode.aspx


我受困于.NET 3.5,因此我无法访问Tuples,所以这是一个不错的解决方案!
aarona

令我惊讶的是,这还没有得到支持。这是一个简单的解决方案,比Tuple更具可读性。
2013年

1
根据msdn,如果没有字段是引用类型,则此方法执行正常,否则,将反射用于相等性。
Gregor Slavec

@Mark结构的问题是它的默认GetHashCode()实现实际上不能保证使用结构的所有字段(导致差的字典性能),而Tuple提供了这样的保证。我已经测试过了 有关详细信息,请参见stackoverflow.com/questions/3841602/…
Eldritch难题,2014年

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)

Perfwise,新的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似乎是复合字典键问题的一种相对优雅的解决方案。
Quarkly

5

立刻想到两种方法:

  1. 按照Kevin的建议做,并编写一个结构作为您的关键。确保使该struct实现IEquatable<TKey>并覆盖其EqualsGetHashCode方法*。

  2. 编写一个在内部利用嵌套字典的类。像这样:TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>...此类在内部将具有type的成员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从对象派生密钥的地方。
在幕后是Dictionary
不必重复Keyand中的键Value
为什么要抓住机会,关键是不是在同一KeyValue
不必在内存中重复相同的信息。

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 2014年

2

我可以建议一个替代方法-一个匿名对象。与在具有多个键的GroupBy LINQ方法中使用的方法相同。

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

可能看起来很奇怪,但是我已经对Tuple.GetHashCode和new {a = 1,b = 2}进行了基准测试。GetHashCode方法和匿名对象在.NET 4.5.1的计算机上都可以使用:

对象-在1000个周期内进行10000次呼叫的89,1732 ms

元组-738,4475毫秒,用于1000个周期的10000次调用


天哪,这种选择我从未想过...我不知道如果您使用复杂类型作为复合键,它是否会表现良好。
加百利·埃斯皮诺萨

如果仅传递一个对象(而不是匿名对象),则将使用该对象的GetHashCode方法的结果。如果像这样使用它,dictionary[new { a = my_obj, b = 2 }]则生成的哈希码将是my_obj.GetHashCode和((Int32)2).GetHashCode的组合。
Michael Logutov

不要使用这种方法!不同的程序集为匿名类型创建不同的名称。尽管它对您来说是匿名的,但在后台却创建了一个具体的类,并且两个不同类的两个对象将与默认运算符不相等。
Quarkly

在这种情况下,这有什么关系呢?
Michael Logutov

0

解决方案的另一种解决方案是存储到目前为止已生成的所有键的某种列表,并在生成新对象时生成它的哈希码(仅作为起点),检查它是否已在列表中,是否已经存在是,然后向其中添加一些随机值,直到获得唯一的键,然后将该键存储在对象本身和列表中,并始终将其作为键返回。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.