【问题标题】:Tuples( or arrays ) as Dictionary keys in C#元组(或数组)作为 C# 中的字典键
【发布时间】:2010-10-31 15:45:16
【问题描述】:

我正在尝试在 C# 中创建字典查找表。我需要将一个三元组的值解析为一个字符串。我尝试使用数组作为键,但这不起作用,我不知道还能做什么。在这一点上,我正在考虑制作一个词典词典,但这可能不是很漂亮,尽管我会在 javascript 中这样做。

【问题讨论】:

    标签: c# dictionary hashtable tuples


    【解决方案1】:

    如果您使用的是 .NET 4.0,请使用元组:

    lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
    

    如果不是,您可以定义一个Tuple 并将其用作键。元组需要覆盖GetHashCodeEqualsIEquatable

    struct Tuple<T, U, W> : IEquatable<Tuple<T,U,W>>
    {
        readonly T first;
        readonly U second;
        readonly W third;
    
        public Tuple(T first, U second, W third)
        {
            this.first = first;
            this.second = second;
            this.third = third;
        }
    
        public T First { get { return first; } }
        public U Second { get { return second; } }
        public W Third { get { return third; } }
    
        public override int GetHashCode()
        {
            return first.GetHashCode() ^ second.GetHashCode() ^ third.GetHashCode();
        }
    
        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
            {
                return false;
            }
            return Equals((Tuple<T, U, W>)obj);
        }
    
        public bool Equals(Tuple<T, U, W> other)
        {
            return other.first.Equals(first) && other.second.Equals(second) && other.third.Equals(third);
        }
    }
    

    【讨论】:

    • 这个结构也应该实现 IEquatable>。这样,在哈希码冲突的情况下调用 Equals() 时,您可以避免装箱。
    • @jerryjvl 和其他所有像我一样通过 Google 找到此内容的人,.NET 4 的元组 implements equals,因此可以在字典中使用。
    • 您的GetHashCode 实现不是很好。它在字段的排列下是不变的。
    • 元组不应该是结构。在框架中,Tuple 是一种引用类型。
    • @Thoraot - 你的例子当然是错误的......它应该是。为什么new object() 等于另一个new object()?它不只是使用直接引用comarison ...尝试:bool test = new Tuple&lt;int, string&gt;(1, "foo").Equals(new Tuple&lt;int, string&gt;(1, "Foo".ToLower()));
    【解决方案2】:

    如果您使用的是 C# 7,则应考虑使用值元组作为复合键。值元组通常比传统的引用元组 (Tuple&lt;T1, …&gt;) 提供更好的性能,因为值元组是值类型(结构),而不是引用类型,因此它们避免了内存分配和垃圾收集成本。此外,它们提供更简洁和更直观的语法,如果您愿意,可以命名它们的字段。他们还实现了字典所需的IEquatable&lt;T&gt; 接口。

    var dict = new Dictionary<(int PersonId, int LocationId, int SubjectId), string>();
    dict.Add((3, 6, 9), "ABC");
    dict.Add((PersonId: 4, LocationId: 9, SubjectId: 10), "XYZ");
    var personIds = dict.Keys.Select(k => k.PersonId).Distinct().ToList();
    

    【讨论】:

    • 实际上,在处理密钥中的大量变量时,元组可能会更快。在某些情况下,复制一个巨大的结构会更慢。
    • @FelixK.:从值类型切换到引用类型的截止点generally recommended 是 16 个字节。一个 int 的 3 元组只占用 12 个字节,所以 ValueTuple 就可以了。但是,即使对于较大的 n 元组,我也会对 Tuple 保持警惕,因为字典查找键通常非常短暂,如果这些查找发生在热路径中,这将对垃圾收集造成很大压力。跨度>
    • 这取决于用例,根据我的经验,大多数时候你可以很好地处理对象而不会出现 GC 问题。我曾经写过一个商业 3d 引擎,所以我必须尽可能优化。如果用例允许,您也可以使用可重复使用的密钥,但我从来不需要这样做。在 90% 的情况下,结构都很好,还有其他方面可以优化。
    • 遗憾的是文档对所涉及的实际哈希算法如此不透明docs.microsoft.com/en-us/dotnet/api/…
    【解决方案3】:

    在基于元组的方法和基于嵌套字典的方法之间,使用基于元组的方法几乎总是更好。

    从可维护性的角度来看

    • 实现如下所示的功能要容易得多:

      var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
      

      var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();
      

      从被调用方。在第二种情况下,每次添加、查找、删除等都需要对多个字典进行操作。

    • 此外,如果您的复合键将来需要一个更多(或更少)字段,您将需要在第二种情况(嵌套字典)中大量更改代码,因为您必须添加更多嵌套字典和后续检查。

    从性能角度来看,您可以得出的最佳结论是自己衡量。但是您可以事先考虑一些理论上的限制:

    • 在嵌套字典的情况下,为每个键(外部和内部)添加一个额外的字典会产生一些内存开销(比创建元组的开销更大)。

    • 在嵌套字典的情况下,添加、更新、查找、删除等每个基本操作都需要在两个字典中执行。现在有一种情况,嵌套字典方法可以更快,即,当正在查找的数据不存在时,因为中间字典可以绕过完整的哈希码计算和比较,但是应该再次确定它的时间。在存在数据的情况下,它应该会更慢,因为查找应该执行两次(或三次,具体取决于嵌套)。

    • 关于元组方法,自 Equals and GetHashCode implementation causes boxing for value types 以来,当 .NET 元组被用作集合中的键时,它们的性能并不是最高的。

    我会使用基于元组的字典,但如果我想要更高的性能,我会使用我自己的元组,实现更好。


    顺便说一句,很少有化妆品可以让字典变得很酷:

    1. Indexer 风格的调用可以更加简洁和直观。例如,

      string foo = dict[a, b, c]; //lookup
      dict[a, b, c] = ""; //update/insertion
      

      因此,在内部处理插入和查找的字典类中公开必要的索引器。

    2. 另外,实现一个合适的IEnumerable 接口并提供一个Add(TypeA, TypeB, TypeC, string) 方法,该方法将为您提供集合初始化语法,例如:

      new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
      { 
          { a, b, c, null }, 
          ...
      };
      

    【讨论】:

    • 在嵌套字典的情况下,索引器语法会不会更像这样:string foo = dict[a][b][c]
    • @StevenRands 是的。
    • @nawfal 当我只有一个键而不是全部时,我可以搜索元组字典吗?或者我可以这样做 dict[a,b] 然后 dict[a,c] 吗?
    • @KhanEngineer 这在很大程度上取决于字典的预期用途或您打算如何使用它。例如,您想通过密钥的一部分 a 取回价值。您可以像任何普通集合一样迭代任何字典并检查键属性是否为a。如果您总是想通过第一个属性获取 dict 中的项目,那么您可以更好地将字典设计为字典的字典,如我的回答和查询中所示,如dict[a],它为您提供了另一个字典。
    • 如果“仅通过一个键搜索”是指通过您拥有的任何键获取值,那么您最好将您的字典重新设计为一种“任何键字典”。例如如果您想为两个键ab 获取值4,那么您可以将其设为标准字典并添加诸如dict[a] = 4dict[b] = 4 之类的值。如果在逻辑上你的ab 应该是一个单元,这可能没有意义。在这种情况下,您可以定义一个自定义的IEqualityComparer,如果它们的任何属性相等,则它等同于两个键实例。所有这些通常都可以通过反射来完成。
    【解决方案4】:

    好的、干净、快速、简单和易读的方法是:

    • 为当前类型生成相等成员(Equals() 和 GetHashCode()) 方法。像ReSharper 这样的工具不仅可以创建方法,还可以为相等性检查和/或计算哈希码生成必要的代码。生成的代码将比 Tuple 实现更优化。
    • 只需创建一个从元组派生的简单键类

    添加类似这样的内容:

    public sealed class myKey : Tuple<TypeA, TypeB, TypeC>
    {
        public myKey(TypeA dataA, TypeB dataB, TypeC dataC) : base (dataA, dataB, dataC) { }
    
        public TypeA DataA => Item1; 
    
        public TypeB DataB => Item2;
    
        public TypeC DataC => Item3;
    }
    

    所以你可以将它与字典一起使用:

    var myDictinaryData = new Dictionary<myKey, string>()
    {
        {new myKey(1, 2, 3), "data123"},
        {new myKey(4, 5, 6), "data456"},
        {new myKey(7, 8, 9), "data789"}
    };
    
    • 您也可以在合同中使用它
    • 作为 linq 中加入或分组的键
    • 这样你永远不会打错 Item1、Item2、Item3 的顺序 ...
    • 您无需记住或查看代码即可了解从何处获取内容
    • 无需重写 IStructuralEquatable、IStructuralComparable、 IComparable,ITuple 他们都已经在这里了

    【讨论】:

    • 现在您可以使用更加简洁的表达式主体成员,例如public TypeA DataA =&gt; Item1;
    【解决方案5】:

    如果出于某种原因您真的想避免创建自己的 Tuple 类,或者使用内置于 .NET 4.0 中的 on,还有另一种方法可能;您可以将三个键值组合成一个值。

    例如,如果三个值是整数类型,不超过 64 位,您可以将它们组合成一个ulong

    在最坏的情况下,您始终可以使用字符串,只要您确保其中的三个组件由键的组件内不出现的某些字符或序列分隔,例如,您可以使用三个数字试试:

    string.Format("{0}#{1}#{2}", key1, key2, key3)
    

    这种方法显然存在一些合成开销,但取决于您使用它的目的,这可能微不足道,无需在意。

    【讨论】:

    • 我会说它在很大程度上取决于上下文;如果我要组合三种整数类型,并且性能并不重要,那么它可以完美地工作,并且出错的可能性很小。当然,从 .NET 4 开始,所有这些都是完全多余的,因为 Microsoft 将为我们提供(可能是正确的!)开箱即用的元组类型。
    • 您甚至可以将此方法与JavaScriptSerializer 结合使用,为您连接字符串和/或整数类型的数组。这样,您就不需要自己想出分隔符了。
    • 如果任何键 (key1,key2,key3) 是包含分隔符 ("#") 的字符串,这可能会变得非常混乱
    【解决方案6】:

    这是 .NET 元组供参考:

    [Serializable] 
    public class Tuple<T1, T2, T3> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple {
    
        private readonly T1 m_Item1; 
        private readonly T2 m_Item2;
        private readonly T3 m_Item3; 
    
        public T1 Item1 { get { return m_Item1; } }
        public T2 Item2 { get { return m_Item2; } }
        public T3 Item3 { get { return m_Item3; } } 
    
        public Tuple(T1 item1, T2 item2, T3 item3) { 
            m_Item1 = item1; 
            m_Item2 = item2;
            m_Item3 = item3; 
        }
    
        public override Boolean Equals(Object obj) {
            return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);; 
        }
    
        Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) { 
            if (other == null) return false;
    
            Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;
    
            if (objTuple == null) {
                return false; 
            }
    
            return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2) && comparer.Equals(m_Item3, objTuple.m_Item3); 
        }
    
        Int32 IComparable.CompareTo(Object obj) {
            return ((IStructuralComparable) this).CompareTo(obj, Comparer<Object>.Default);
        }
    
        Int32 IStructuralComparable.CompareTo(Object other, IComparer comparer) {
            if (other == null) return 1; 
    
            Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;
    
            if (objTuple == null) {
                throw new ArgumentException(Environment.GetResourceString("ArgumentException_TupleIncorrectType", this.GetType().ToString()), "other");
            }
    
            int c = 0;
    
            c = comparer.Compare(m_Item1, objTuple.m_Item1); 
    
            if (c != 0) return c; 
    
            c = comparer.Compare(m_Item2, objTuple.m_Item2);
    
            if (c != 0) return c; 
    
            return comparer.Compare(m_Item3, objTuple.m_Item3); 
        } 
    
        public override int GetHashCode() { 
            return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
        }
    
        Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer) { 
            return Tuple.CombineHashCodes(comparer.GetHashCode(m_Item1), comparer.GetHashCode(m_Item2), comparer.GetHashCode(m_Item3));
        } 
    
        Int32 ITuple.GetHashCode(IEqualityComparer comparer) {
            return ((IStructuralEquatable) this).GetHashCode(comparer); 
        }
        public override string ToString() {
            StringBuilder sb = new StringBuilder();
            sb.Append("("); 
            return ((ITuple)this).ToString(sb);
        } 
    
        string ITuple.ToString(StringBuilder sb) {
            sb.Append(m_Item1); 
            sb.Append(", ");
            sb.Append(m_Item2);
            sb.Append(", ");
            sb.Append(m_Item3); 
            sb.Append(")");
            return sb.ToString(); 
        } 
    
        int ITuple.Size { 
            get {
                return 3;
            }
        } 
    }
    

    【讨论】:

    • 获取哈希码实现为 ((item1 ^ item2) * 33) ^ item3
    【解决方案7】:

    我会用适当的 GetHashCode 覆盖您的元组,并将其用作键。

    只要重载正确的方法,您应该会看到不错的性能。

    【讨论】:

    • IComparable 不会影响键在 Dictionary 中的存储或定位方式。这一切都是通过 GetHashCode() 和 IEqualityComparer 完成的。实现 IEquatable 将获得更好的性能,因为它减轻了由默认 EqualityComparer 引起的装箱,该装箱依赖于 Equals(object) 函数。
    • 我正要提到 GetHashCode,但我认为字典在 HashCode 相同的情况下使用了 IComparable……我想我错了。
    【解决方案8】:

    如果您的消费代码可以使用 IDictionary 接口而不是 Dictionary,我的直觉是使用带有自定义数组比较器的 SortedDictionary,即:

    class ArrayComparer<T> : IComparer<IList<T>>
        where T : IComparable<T>
    {
        public int Compare(IList<T> x, IList<T> y)
        {
            int compare = 0;
            for (int n = 0; n < x.Count && n < y.Count; ++n)
            {
                compare = x[n].CompareTo(y[n]);
            }
            return compare;
        }
    }
    

    并因此创建(使用 int[] 只是为了具体示例):

    var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
    

    【讨论】:

      【解决方案9】:

      所以最新的答案是改用数组。创建这个类:

              class StructuralEqualityComparer<T> : EqualityComparer<T[]>
              {
                  public override bool Equals(T[] x, T[] y)
                  {
                      return StructuralComparisons.StructuralEqualityComparer
                          .Equals(x, y);
                  }
      
                  public override int GetHashCode(T[] obj)
                  {
                      return StructuralComparisons.StructuralEqualityComparer
                          .GetHashCode(obj);
                  }
              }
      

      然后像这样使用它:

      var dict = new Dictionary<object[], SomeOtherObject>(new StructuralEqualityComparer<object>())
      

      该字典将正确调用 GetHashCode 以获取数组的最后(我相信)8 个元素。这已经足够了,因为哈希码不是唯一的,但我们需要字典来获取它们。以及一些将它们组合起来的代码。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-09-25
        • 1970-01-01
        • 2019-02-12
        • 2011-11-01
        • 2010-11-28
        • 1970-01-01
        • 2019-03-28
        • 1970-01-01
        相关资源
        最近更新 更多