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


109

我正在尝试在C#中创建一个字典查找表。我需要将三元组的值解析为一个字符串。我尝试使用数组作为键,但是那没有用,而且我不知道该怎么办。在这一点上,我正在考虑制作“字典词典”,尽管我将使用javascript做到这一点,但看起来可能不太漂亮。

Answers:


114

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

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

如果不是,则可以定义一个元组并将其用作键。元组需要重写GetHashCode,Equals和IEquatable:

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

6
此结构还应实现IEquatable <Tuple <T,U,W >>。这样,您可以避免在散列代码冲突的情况下调用Equals()时装箱。
达斯汀·坎贝尔

16
@jerryjvl以及其他像我一样在Google上找到它的人,.NET 4的Tuple 实现了equals,因此可以在字典中使用。
Scott Chamberlain

30
您的GetHashCode实现不是很好。在字段置换下它是不变的。
CodesInChaos

2
元组不应该是一个结构。在框架中,Tuple是引用类型。
Michael Graczyk 2012年

5
@Thoraot-当然,您的示例是错误的……应该如此。为什么会new object()等于另一个new object()?它不仅仅使用直接参考比较...尝试:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Mike Marynowski

35

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

从可维护性的角度来看

  • 实现看起来像这样的功能要容易得多:

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

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

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

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

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

  • 在嵌套字典的情况下,为每个键(外部和内部)都拥有一个附加的字典将有一些内存开销(比创建一个元组所需要的更多)。

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

  • 关于元组的方法,.NET元组是不是最高效的,当他们命中注定要被用作自钥匙套EqualsGetHashCode实施的原因拳击值类型

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


附带说明一下,很少有化妆品能使字典变得很酷:

  1. 索引器样式的调用可以更加简洁直观。例如

    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

1
@nawfal当我只有一个键而不是全部键时,可以搜索元组字典吗?还是我喜欢这个dict [a,b]然后dict [a,c]吗?
可汗工程师

@KhanEngineer在很大程度上取决于字典的预期用途或使用方式。例如,您想通过一部分钥匙取回价值a。您可以像对任何普通集合一样对任何字典进行迭代,并检查是否为key属性a。如果您始终希望通过第一个属性来获取字典中的项目,则可以更好地将字典设计为字典的字典,如我的答案所示,并像这样查询dict[a],这将为您提供另一个字典。
nawfal

如果通过“仅搜索一个键”来表示要通过您拥有的任何键取回值,那么最好将字典重新设计为一种“任何键字典”。例如,如果您想同时获得4a和的值b,则可以将其设置为标准字典并添加诸如dict[a] = 4和的值dict[b] = 4。它可能没有意义,如果你的逻辑a,并b应该是一个单位。在这种情况下,您可以定义一个自定义IEqualityComparer,如果两个键实例的任何一个属性相等,则该键将两个键实例等效。所有这些通常都可以通过反射来完成。
nawfal

30

如果您使用的是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();

13

良好,干净,快速,简单和易读的方式是:

  • 生成当前类型的相等成员(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在这里它们都变质了

1
现在,您可以使用身体更public TypeA DataA => Item1;
柔和的

7

如果出于某种原因您确实想要避免创建自己的Tuple类或在.NET 4.0中内置使用,则可以使用其他方法。您可以将三个键值组合在一起成为一个值。

例如,如果这三个值是整数类型,且总和不超过64位,则可以将它们组合为ulong

在最坏的情况下,您始终可以使用字符串,只要确保其中的三个组成部分由键的组成部分内没有出现的某些字符或序列定界即可,例如,可以尝试使用三个数字:

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

显然,这种方法有一些合成开销,但是取决于您为此使用的内容,可能微不足道,无需理会。


6
我会说,这在很大程度上取决于上下文。如果我有三种整数类型可以组合,并且性能不是很关键,则可以很好地工作,并且犯错的可能性很小。当然,从.NET 4开始,所有这些都是完全多余的,因为Microsoft将为我们提供(可能是正确的!)元组类型。
jerryjvl

您甚至可以将此方法与结合使用,为JavaScriptSerializer您串联一个字符串和/或整数类型的数组。这样,您无需自己提出定界符。
binki 2014年

3
这可以得到真正的混乱,如果任何键(中key1key2key3)分别含deliminator(串"#"
格雷格-

4

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

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


1
IComparable对密钥的存储方式或在Dictionary <TKey,TValue>中的位置没有影响。全部通过GetHashCode()和IEqualityComparer <T>完成。实现IEquatable <T>将获得更好的性能,因为它减轻了默认EqualityComparer导致的装箱,该装箱依赖Equals(object)函数。
达斯汀·坎贝尔

我本来要提到GetHashCode,但是我认为在HashCodes相同的情况下,Dictionary使用IComparable……我猜错了。
约翰·吉岑(Jietzen)2009年

3

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

3
获取哈希码的实现方式为((item1 ^ item2)* 33)^ item3
Michael

2

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