【问题标题】:Correct usage of custom data structure for Multi Key Dictionary正确使用多键字典的自定义数据结构
【发布时间】:2016-08-29 13:21:58
【问题描述】:

在我的应用程序的某个时刻,我遇到了一个类实例需要三个字符串键的问题(我使用的是 C# 3.5,所以我不能使用元组)。通过在线查找,我发现了这个答案,我使用了它的代码:https://stackoverflow.com/a/15804355/5090537

在根据我的需要定制它的点点滴滴之后,最后我的自定义类看起来像这样:

public class MultiKeyDictionary<K1, K2, K3, V> : Dictionary<K1, MultiKeyDictionary<K2, K3, V>>
{
    public V this[K1 key1, K2 key2, K3 key3]
    {
        get
        {
            return ContainsKey(key1) ? this[key1][key2, key3] : default(V);
        }
        set
        {
            if (!ContainsKey(key1))
                this[key1] = new MultiKeyDictionary<K2, K3, V>();
            this[key1][key2, key3] = value;
        }
    }

    public bool ContainsKey(K1 key1, K2 key2, K3 key3)
    {
        return base.ContainsKey(key1) && this[key1].ContainsKey(key2, key3);
    }

    public void Add(K1 key1, K2 key2, K3 key3, V value)
    {
        if (!ContainsKey(key1))
            this[key1] = new MultiKeyDictionary<K2, K3, V>();
        if (!this[key1].ContainsKey(key2, key3))
            this[key1][key2] = new Dictionary<K3, V>();
        this[key1][key2][key3] = value;
    }
}

这非常适合我的需求,但我对此数据结构有一些疑问:

1) 由于我实际上是从Dictionary(K1, Dictionary(K2, V)) 继承的,因此假设为我实现了GetHashCode 并且我不需要指定单独的实现是否正确? Equals 也一样?

2) 我需要创建自己的自定义类的前提也是正确的吗?例如,因为我不能使用字符串数组或字符串列表,因为那样会有ReferenceEquals 比较而不是我需要的成员比较(key1 等于 key1,key2 等于 key2,key3 等于 key3) ?

【问题讨论】:

  • 使用Dictionary&lt;Tuple&lt;string,string,string&gt;,YourClass&gt; 直接解决方案,或者您也可以使用List&lt;Tuple&lt;string,string,string,YourClass&gt;&gt;
  • @HadiHassan :问题中专门解决了这个问题 - 在 C# 4 之前,Tuple 类不可用。
  • @GaryMcGill 我认为 3 个键的元组可以很容易地完成,但我不认为构建一个数据结构(字典字典的字典)来表示具有 3 个键的数据是一个不错的选择在这里,(要使用或从头开始实现采用 3 个键和一个对象值的元组类更容易和直接)。没仔细看问题,直接看代码和下面。
  • 我第二个@HadiHassan。从三个字符串创建一个(相当简单的)结构(当然,正确定义相等和散列),基本上模拟Tuple;并使用该结构作为 一维 Dictionary. 中的键

标签: c# .net dictionary hashcode


【解决方案1】:

好吧,创建自己的三键结构来存储键是一个不错的计划,但首先让我们看一下source codeKeyValuePair 结构。

现在让我们定义我们自己的 TripleKey 结构体:

[Serializable]
public struct TripleKey<TKeyA, TKeyB, TKeyC>
{
    public TKeyA KeyA { get; };
    public TKeyB KeyB { get; };
    public TKeyC KeyC { get; };

    public TripleKey(TKeyA keyA, TKeyB keyB, TKeyC keyC)
    {
        this.KeyA = keyA;
        this.KeyB = keyB;
        this.KeyC = keyC;
    }

    // this code is almost the same as it is in Microsoft implementation
    public override string ToString()
    {
        var sBuilder = new StringBuilder();
        sBuilder.Append('(');
        if (KeyA != null)
        {
            sBuilder.Append(KeyA.ToString());
        }
        sBuilder.Append(", ");
        if (KeyB != null)
        {
            sBuilder.Append(KeyB.ToString());
        }
        sBuilder.Append(", ");
        if (KeyC != null)
        {
            sBuilder.Append(KeyC.ToString());
        }
        sBuilder.Append(')');
        return sBuilder.ToString();
    }
}

public static class TripleKey
{
    public static TripleKey<TKeyA, TKeyB, TKeyC> Create<TKeyA, TKeyB, TKeyC>(TKeyA keyA, TKeyB keyB, TKeyC keyC)
    {
        return new TripleKey<TKeyA, TKeyB, TKeyC>(keyA, keyB, keyC);
    }
}

public class MultiKeyDictionary<TKeyA, TKeyB, TKeyC, TValue> : Dictionary<TripleKey<TKeyA, TKeyB, TKeyC>, TValue>
{
    public TValue this[TKeyA keyA, TKeyB keyB, TKeyC keyC]
    {
        get
        {
            var key = TripleKey.Create(keyA, keyB, keyC);
            return base.ContainsKey(key) ? base[key] : default(TValue);
        }
        set
        {
            var key = TripleKey.Create(keyA, keyB, keyC);
            if (!ContainsKey(key))
                base.Add(key, value);

            this[key] = value;
        }
    }

    public bool ContainsKey(TKeyA keyA, TKeyB keyB, TKeyC keyC)
    {
        var key = TripleKey.Create(keyA, keyB, keyC);

        return base.ContainsKey(key);
    }

    public void Add(TKeyA keyA, TKeyB keyB, TKeyC keyC, TValue value)
    {
        base.Add(TripleKey.Create(keyA, keyB, keyC), value);
    }
}

结构类型最大的优点之一是因为它们继承自ValueType,所以它们也继承了GetHashCode 方法的实现。这个实现的工作方式是,对于任何两个具有相同值的结构,它们的哈希码总是匹配的(相反的情况并不总是如此,但如果两个哈希码匹配,则不能百分百保证所有值都相同)。

现在,当我们都安顿好后,我们可以使用MultiKeyDictionary&lt;TKeyA, TKeyB, TKeyC, TValue&gt; 或简单的Dictionary&lt;TripleKey&lt;TKeyA, TKeyB, TKeyC&gt;, TValue&gt;

一个简单的例子:

var myDict = new MultiKeyDictionary<string, double, double, string>
{
    {"Goodbye", 0.55, 9.00, "yaya"} // collection initializer works fine
};

myDict.Add("Hello", 1.11, 2.99, "hi");

Console.WriteLine(myDict.ContainsKey("Hello", 1.11, 2.99));  // true
Console.WriteLine(myDict.ContainsKey("a", 1.11, 2.99));      // false
Console.WriteLine(myDict["Hello", 1.11, 2.99]);              // hi

myDict.Add(TripleKey.Create("Hello", 1.11, 2.99), "gh");     // bang! exception, 
                                                             // key already exists

附言

正如 ScottChamberlain 所正确指出的那样,ValueTypeimplementation of GetHashcode 方法各有利弊。它使用反射,这意味着它可能会导致性能问题,因此在某些情况下,最好不要依赖 struct 的 GetHashCode 实现,而是使用自定义实现覆盖它。

Eric Lippert 的博客中有一篇很棒的文章,叫做“Guidelines and rules for GetHashCode”。

工作示例:https://dotnetfiddle.net/y1a30V

【讨论】:

  • 如果您依赖继承的GetHashCode 实现来实现struct,请小心。 It might not do what you think!
  • 自己实现GetHashCode() 可能是值得的。默认实现将使用反射,如果您执行大量操作,这可能会成为性能瓶颈。
  • 但另一方面,据我所知KeyValuePair 方法GetHashCode() 没有实现。
  • @Fabjan 那是因为它不会将KeyValuePair 本身存储在字典the pair is constructed at the time of the request 中。
  • @GaryMcGill ValueType has it's own override,你使用的不是Object的实现。
【解决方案2】:

GetHashCode

GetHashCode 方法用作测试类的两个实例是否相等的“廉价”(快速)方法。为两个相等的实例调用GetHashCode 应该总是 产生相同的结果。因此,如果两个实例调用GetHashCode 的结果不同,那么它们就不可能相等,因此没有必要进行更详细(也更“昂贵”)的比较。

[另一方面,如果两个实例具有相同的哈希码,则需要进行更详细的比较以确定它们实际上是否相等。]

因此,除非您重新定义“平等”对您的班级意味着什么,否则您可能不需要担心GetHashCode。无论如何,您班级的“平等”概念似乎并不是很有用。

类设计

我不确定您实现的类是否理想。因为您继承自 Dictionary,所以您继承了一些并不真正“适合”您的类的方法。

例如,您的类现在有一个 Keys 方法,该方法返回顶级键 (key1),而不是您的类实际表示的三值键。

我想知道实现一个聚合字典而不是继承字典的类是否会更好。

如果没有Tuple,另一种选择是定义您自己的TriKey&lt;K1, K2, K3&gt; 类(具有描述您的键值的3 个属性),然后使用Dictionary&lt;TriKey&lt;K1, K2, K3&gt;, V&gt;。在这种情况下,您绝对想要为您的 TriKey 类定义相等,并且您需要使 GetHashCode 与相等的定义保持一致,因为字典查找是其中之一被使用了。

杂项

最后一点,有些人可能会认为为时过早优化。代码:

this[key1][key2][key3] = value;

... 将对您已经获取的值执行 2 次查找(因为您已经访问了 this[key1]this[key1][key2])。您可能需要考虑使用局部变量来存储这些中间结果。

例如:

MultiKeyDictionary<K2, K3, V> d1;
if (!TryGetValue(key1, out d1))
{
    d1 = new MultiKeyDictionary<K2, K3, V>();
    this[key1] = d1;
}

// now use d1 rather than "this[key1]"

... 以此类推。

【讨论】:

  • 我喜欢你使用我自己的 TriKey 类的想法。有关如何实施和处理 GetHashCode 的任何建议/指南?
  • @lason :请参阅 Eric Lippert's post 以获取有关实现 GetGashCode 的建议,以及 Marc Gravell's answer 以获取有关如何组合多个值以生成复合哈希码的示例。
【解决方案3】:

这可能是完成您所追求的最简单的方法:

public class MultiKeyDictionary<TKey, TValue> : Dictionary<Tuple<TKey, TKey, TKey>, TValue>
{
    public MultiKeyDictionary()
        : base()
    {
    }
    ...
}

class Program
{
    static void Main(string[] args)
    {
        // C# 6.0 syntax
        var multiKeyDictionary = new MultiKeyDictionary<string, int>();
        multiKeyDictionary.Add(Tuple.Create("key1", "key2", "key3"), 36);

        // C# 7.0 syntax (not yet released).
        var multiKeyDictionary1 = new MultiDictionary<string, int>();
        multiKeyDictionary1.Add(("key1", "key2", "key3"), 36);
    }
}

当 C# 7.0 发布时,您可以使用漂亮的新元组声明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-04-06
    • 1970-01-01
    • 2013-10-12
    • 2017-04-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多