【问题标题】:Composite Key Dictionary复合键字典
【发布时间】:2011-02-22 02:07:54
【问题描述】:

我在 List 中有一些对象,比如说 List<MyClass> 和 MyClass 有几个属性。我想根据 MyClass 的 3 个属性创建列表索引。在这种情况下,其中 2 个属性是 int,一个属性是 datetime。

基本上我希望能够做类似的事情:

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

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

【问题讨论】:

  • 为什么不使用元组?他们为您完成所有合成。
  • 对不起,我重写了它作为更详细的答案。
  • 在实现自定义类之前,请阅读有关 Tuple 的信息(如 Eldritch Conundrum 所建议的)-msdn.microsoft.com/en-us/library/system.tuple.aspx。它们更容易更改,并且可以节省您创建自定义类的时间。

标签: c# dictionary


【解决方案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 年编辑 **

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

Tuple&lt;int, bool, string&gt; 的类型变为(int, bool, string)Tuple.Create(4, true, "t") 的值变为(4, true, "t")

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

【讨论】:

  • 元组不是一个很好的键候选,因为它会产生大量的哈希冲突。 stackoverflow.com/questions/12657348/…
  • @Blam KeyValuePair&lt;K,V&gt; 和其他结构有一个默认的散列函数,该函数已知是错误的(有关更多详细信息,请参阅stackoverflow.com/questions/3841602/…)。但是Tuple&lt;&gt; 不是ValueType,它的默认哈希函数至少会使用所有字段。话虽如此,如果您的代码的主要问题是冲突,那么请实现适合您数据的优化GetHashCode()
  • 即使 Tuple 在我的测试中不是一个 ValueType,它也会遭受很多冲突
  • 我认为这个答案已经过时了,因为我们有了 ValueTuples。它们在 C# 中有更好的语法,而且它们的 GetHashCode 似乎是元组的两倍——gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
  • @LucianWischik 谢谢,我已经更新了答案以提及他们。
【解决方案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;
        }
    }
}

关于 GetHashCode() 的 MSDN 文章:

http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx

【讨论】:

  • 我不认为这实际上是 100% 肯定是唯一的哈希码,只是很有可能。
  • 这很可能是真的!根据链接的 MSDN 文章,这是覆盖 GetHashCode() 的推荐方法。但是,由于我在日常工作中使用的复合键并不多,所以我不能肯定。
  • 是的。如果你用 Reflector 反汇编 Dictionary.FindEntry(),你会看到哈希码和完全相等都被测试了。首先测试哈希码,如果失败,则在不检查完全相等的情况下使条件短路。如果哈希通过,则也测试相等性。
  • 是的,equals 也应该被覆盖以匹配。即使你让 GetHashCode() 为任何实例返回 0,Dictionary 仍然可以工作,只是速度会慢一些。
  • 内置元组类型将哈希组合实现为 '(h1
【解决方案3】:

Dictionary&lt;int, Dictionary&lt;int, Dictionary&lt;DateTime, MyClass&gt;&gt;&gt;怎么样?

这将允许您这样做:

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

【讨论】:

  • 这将创建更多的对象,然后使用 CompositeKey 结构或类。并且也会因为使用两级查找而变慢。
  • 我相信这是相同数量的比较 - 我看不出会有更多的对象 - 复合键方式仍然需要一个键,它是组件值或对象和一个 dict捉住它。这种嵌套方式,您不需要每个对象/值的包装键,每个额外的嵌套级别都需要一个额外的字典。你怎么看?
  • 基于我的基准测试,我尝试使用具有 2 部分和 3 部分的键:嵌套字典解决方案比使用元组复合键方法快 3-4 倍。但是,元组方法更容易/更整洁。
  • @RickL 我可以确认这些基准,我们在代码库中使用了一种类型,称为 CompositeDictionary&lt;TKey1, TKey2, TValue&gt; (等),它简单地继承自 Dictionary&lt;TKey1, Dictionary&lt;TKey2, TValue&gt;&gt; (或者需要许多嵌套字典。没有我们自己从头开始实现整个类型(而不是使用嵌套字典或类型来包含键)这是我们得到的最快的。
  • 嵌套字典方法应该只在一半(?)数据不存在的情况下更快,因为中间字典可以绕过完整的哈希码计算和比较。在存在数据的情况下,它应该会更慢,因为添加、包含等基本操作应该执行三次。我确信在上面提到的一些基准测试中,元组方法的优势是关于 .NET 元组的实现细节,考虑到它为值类型带来的装箱惩罚,这非常糟糕。考虑到内存,我会选择正确实现的三元组
【解决方案4】:

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

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

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

【讨论】:

  • 我被困在 .NET 3.5 上,所以我无法访问 Tuples,所以这是一个很好的解决方案!
  • 我很惊讶这没有得到更多的支持。这是一个比元组更具可读性的简单解决方案。
  • 根据msdn,如果没有字段是引用类型,则执行正常,否则使用反射进行相等。
  • @Mark 结构体的问题在于其默认的 GetHashCode() 实现实际上并不能保证使用结构体的所有字段(导致字典性能不佳),而 Tuple 提供了这样的保证。我已经测试过了。有关详细信息,请参阅stackoverflow.com/questions/3841602/…
【解决方案5】:

既然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

【讨论】:

  • 是的,我进行了一次大的重写,只是为了让匿名类型解决方案在我面前崩溃(无法比较使用不同程序集创建的匿名类型)。 ValueTuple 似乎是解决复合字典键问题的一个相对优雅的解决方案。
【解决方案6】:

两种方法立即浮现在脑海:

  1. 按照 Kevin 的建议进行操作,并编写一个结构作为您的密钥。请务必使此结构实现 IEquatable&lt;TKey&gt; 并覆盖其 EqualsGetHashCode 方法*。

  2. 编写一个在内部使用嵌套字典的类。类似于:TripleKeyDictionary&lt;TKey1, TKey2, TKey3, TValue&gt;... 这个类内部将有一个 Dictionary&lt;TKey1, Dictionary&lt;TKey2, Dictionary&lt;TKey3, TValue&gt;&gt;&gt; 类型的成员,并且会公开诸如 this[TKey1 k1, TKey2 k2, TKey3 k3]ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3) 等方法。

*关于是否需要重写Equals 方法的一句话:虽然结构的Equals 方法默认比较每个成员的值是正确的,但它是通过使用反射来实现的——这本质上需要性能成本 - 因此 不是 非常适合用作字典中的键的东西(在我看来,无论如何)。根据ValueType.Equals上的MSDN文档:

默认实现 Equals 方法使用反射 比较对应的字段 obj 和这个实例。覆盖 特定类型的等于方法 提高方法的性能 更紧密地代表了这个概念 类型的相等性。

【讨论】:

  • 关于 1,我认为你不需要重写 Equals 和 GetHashcode,Equals 的默认实现会自动检查我认为在这个结构上应该没问题的所有字段的相等性。
  • @ho:这可能不是必要的,但我强烈建议对任何将用作键的结构都这样做。查看我的编辑。
【解决方案7】:

如果密钥是类的一部分,则使用KeyedCollection
它是一个Dictionary,其中的键是从对象派生的。
在封面下是字典
不必重复KeyValue 中的密钥。
为什么要冒险,Key 中的密钥与 Value 中的密钥不同。
不必在内存中复制相同的信息。

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,微软特别建议不要使用它。

ValueType.GetHashCode

Tuple 从技术上讲不是值类型,但具有相同的症状(散列冲突)并且不适合用作键。

【讨论】:

  • +1 以获得更正确的答案。令之前没有人提到它感到惊讶。事实上,取决于 OP 打算如何使用该结构,HashSet&lt;T&gt; 和适当的IEqualityComparer&lt;T&gt; 也是一种选择。顺便说一句,如果您可以更改班级名称和其他成员名称,我认为您的回答会吸引选票:)
【解决方案8】:

我可以建议一个替代方案 - 一个匿名对象。这与我们在 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 毫秒

元组 - 1000 个周期内 10000 次调用需要 738,4475 毫秒

【讨论】:

  • omg,我从来没想过这个替代方案......我不知道如果你使用复杂类型作为复合键,它是否会表现良好。
  • 如果您只是传递一个对象(而不是匿名对象),则将使用该对象的 GetHashCode 方法的结果。如果您像dictionary[new { a = my_obj, b = 2 }] 一样使用它,那么生成的哈希码将是 my_obj.GetHashCode 和 ((Int32)2).GetHashCode 的组合。
  • 不要使用这种方法!不同的程序集为匿名类型创建不同的名称。虽然它看起来对您来说是匿名的,但在幕后创建了一个具体的类,并且两个不同类的两个对象与默认运算符不相等。
  • 在这种情况下这有什么关系?
【解决方案9】:

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

【讨论】:

    【解决方案10】:

    作为替代方案:

    也许这会对有这种必要性的人有所帮助。

    一种选择是使用字符串作为字典的复合键。示例:

    var myDict = new Dictionary<string, bool>();
    myDict.Add($"{1}-{1111}-{true}", true);
    myDict.Add($"{1}-{1111}-{false}", false);
    

    通过这种方式,您可以存储任何格式的密钥。如果您愿意,您可以随时定义一个构建密钥的函数:

    string BuildKey(int number, string name, bool disabled) => $"{number}-{name}-{disabled}";
    

    【讨论】:

      猜你喜欢
      • 2013-01-28
      • 1970-01-01
      • 2016-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-04
      • 2018-03-27
      相关资源
      最近更新 更多