元组(或数组)作为 C # 中的字典键

我正在尝试用 C # 创建一个字典查找表。我需要将3个元组的值解析为一个字符串。我试过用数组作为键,但是没有用,我不知道还能做什么。在这一点上,我正在考虑做一个字典的字典,但这可能不是很漂亮看,虽然我会这样做在 javascript。

142215 次浏览

我会用一个合适的 GetHashCode 覆盖你的 Tuple 然后把它当做密钥。

只要重载了正确的方法,就应该会看到良好的性能。

如果你在.NET 4.0上使用 Tuple:

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

如果出于某种原因,您确实希望避免创建自己的 Tuple 类,或使用。NET 4.0,还有另一种可能的方法; 您可以将三个键值组合成一个单独的值。

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

最糟糕的情况是,你总是可以使用一个字符串,只要你确保其中的三个组成部分用一些字符或序列来分隔,而这些字符或序列不会出现在密钥的组成部分中,例如,你可以尝试使用三个数字:

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

这种方法显然存在一些组合开销,但是取决于您为此使用它的内容,这些内容可能微不足道,无需关心。

下面是.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;
}
}
}

如果您的消费代码可以使用 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>());

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

从可维护性的角度来看,

  • 实现这样的功能要容易得多:

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

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

    在第二种情况下,每个添加、查找、删除等都需要对多个字典执行操作。

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

从性能角度来看,您可以得到的最佳结论是通过自己测量它。但是有一些理论上的局限性,你可以事先考虑一下:

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

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

  • 关于元组方法。NET 元组的性能并不是最好的,因为它们本应该被用作集合中的键,因为它的 ABC0和 GetHashCode实现导致对值类型进行装箱

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


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

  1. 索引器样式调用可以更加干净和直观,

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

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

  2. Also, implement a suitable IEnumerable interface and provide an Add(TypeA, TypeB, TypeC, string) method which would give you collection initializer syntax, like:

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

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

  • 为当前类型生成相等成员(Equals ()和 GetHashCode ()) 方法。像 锐利者这样的工具不仅创建方法,而且还生成用于相等性检查和/或计算哈希代码的必要代码。生成的代码将比 Tuple 实现更优化。
  • just make a simple key class derived from a 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 中连接或分组的键
  • 这样你就永远不会错误地输入项目1,项目2,项目3的顺序 ...
  • you no need to remember or look into to code to understand where to go to get something
  • no need to override IStructuralEquatable, IStructuralComparable, 他们都已经在这里了

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

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();

因此,最新的解决方案是使用数组:

        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个元素(我相信是这样的)。这已经足够了,因为散列码不是唯一的,但是我们需要字典来得到它们。还有一些代码来组合它们。