复合键字典

时间:2010-05-20 20:45:20

标签: c# dictionary

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

基本上我希望能够做到这样的事情:

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

我有时会在列表上创建多个字典来索引它所拥有的类的不同属性。我不知道如何最好地处理复合键。我考虑过对三个值进行校验和,但这会产生碰撞的风险。

9 个答案:

答案 0 :(得分:94)

你应该使用元组。它们等同于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&lt; ...&gt;来缩短名称。


** 2017年编辑 **

从C#7开始有一个新选项:值元组。这个想法是一样的,但语法不同,更轻:

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

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

答案 1 :(得分:21)

我能想到的最好的方法是创建一个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/en-us/library/system.object.gethashcode.aspx

答案 2 :(得分:13)

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

这将允许你这样做:

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

答案 3 :(得分:11)

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

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

获取哈希码的链接: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx

答案 4 :(得分:6)

现在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比GetHashCode的Tuple快,但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

答案 5 :(得分:4)

立刻想到两种方法:

  1. 按照凯文的建议并编写一个可作为关键字的结构。请务必使用此结构实现IEquatable<TKey>并覆盖其EqualsGetHashCode方法*。

  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)等方法。

  3. *关于是否需要覆盖Equals方法的说法是正确的:虽然结构的Equals方法默认情况下会比较每个成员的值,但它是通过使用反射来实现的 - 这本身就需要性能成本 - 因此是一种非常合适的实现方式,可用作字典中的键(在我看来,无论如何)。根据{{​​3}}上的MSDN文档:

      

    默认执行   Equals方法使用反射   比较相应的字段   obj和这个例子。覆盖   等于特定类型的方法   提高方法的性能   并且更接近地代表了这个概念   类型的平等。

答案 6 :(得分:3)

如果密钥是该类的一部分,则使用KeyedCollection 它是一个字典,其中键是从对象派生的 在封面下它是词典
不必重复键和值中的键 为什么键中的键与值不一样 不必在内存中复制相同的信息。

KeyedCollection Class

公开复合键的索引器

    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

元组在技术上不是值类型,但是遇到相同的症状(哈希冲突),并且不适合使用密钥。

答案 7 :(得分: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上获胜:

对象 - 1000次循环中10000次调用的89,1732 ms

元组 - 在1000个周期内进行10000次呼叫的738,4475 ms

答案 8 :(得分:0)

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