C# 基于元组或嵌套字典是否有好处?

C# 基于元组或嵌套字典是否有好处?,c#,dictionary,C#,Dictionary,我一直在寻找一种方法来存储和检索C#的泛型字典类提供的不止一个键上的值 在web上搜索()向我展示了几个选项: 基于元组的词典 .NET4.0使支持通用元组类变得容易。这意味着您可以用任意元组制作字典,即 var myDict=newdictionary() 嵌套字典 我了解到还可以在字典中嵌套字典,这使得访问存储结果类似于访问N维数组。例如: Dictionary<int, Dictionary<int, Dictionary<Char, MyClass>>&

我一直在寻找一种方法来存储和检索C#的泛型字典类提供的不止一个键上的值

在web上搜索()向我展示了几个选项:

基于元组的词典

.NET4.0使支持通用元组类变得容易。这意味着您可以用任意元组制作字典,即

  • var myDict=newdictionary()
嵌套字典

我了解到还可以在字典中嵌套字典,这使得访问存储结果类似于访问N维数组。例如:

Dictionary<int, Dictionary<int, Dictionary<Char, MyClass>>>
是否存在使其中一种方法优于另一种方法的情况?他们会有类似的表演时间吗?或者应该把重点放在哪个方法提供最干净、最容易维护的代码上


想法?

分隔的串联键词典

我之所以会避免这种做法,至少有三个原因:

  • 这是魔法。键的类型中没有任何东西可以告诉您如何构造它或它代表什么
  • 如果分隔符意外显示为其中一个值,则您的方法将失败
  • 转换为字符串,并且这些字符串的比较可能(稍微)比使用两个基元类型慢
嵌套字典

这解决了定界符的问题,但也带来了一些新问题:

  • 插入新值很困难,因为对于每个嵌套级别,必须检查该键是否已存在。如果没有,则需要创建一个新字典作为值。这使得使用字典更加困难
  • 这将进一步增加内存和性能开销
基于元组的词典

在你发布的方法中,这可能是最好的


但是,您可以更进一步,为您的密钥创建一个命名的不可变
struct
。这将使您的字典更易于使用,因为键的各个部分可以有有用的名称。

您描述的所有选项都非常相似—至于性能,您需要针对特定的使用场景测试每个选项,但对于小集合,它们不太可能有太大的差异

它们也都有可读性问题——很难构建它们,也很难从类型中挑出含义

相反,最好是创建一个直接描述数据的类型——好的命名有很长的路要走

或者应该把重点放在哪个方法提供最干净、最容易维护的代码上

除非您的重点是编写噩梦般的、令人生畏的代码,否则您应该避免使用字符串分隔和连接方法,这是一种不言而喻的邪恶方法

选择基于元组和嵌套字典的方法取决于您的上下文。调整性能?还是为了可读性而调整?我先谈谈后者

从可维护性的角度来看

  • 实现如下功能更容易:

    var myDict = new Dictionary<Tuple<char, int>, MyClass>();
    
    var myDict=newdictionary();
    

    var myDict=newdictionary();
    
    从被叫方。在第二种情况下,每个添加、查找、删除等操作都需要对多个字典执行

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

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

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

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

  • 关于元组方法,.NET元组不是最有性能的,因为它是在集合中用作键的

总的来说,我发现很少需要嵌套字典方法。人们很可能不想要它。我更喜欢基于元组的方法,但您应该编写一个具有更好实现的自己的元组,在
char
int
键的情况下,我更喜欢使其成为一个(不可变的)结构


一个非常相关的问题:

我想补充上述答案,在某些情况下(取决于数据的分布方式),嵌套字典在内存占用方面要比复合键字典好得多(这反过来可能会提高整体性能)。 这样做的原因是嵌套可以避免为键保存重复值的需要,这在大型字典中会使额外字典的占用空间可以忽略不计

例如,假设我需要一本复合键为(男/女),(婴儿/年轻/老年),(年龄)的字典

让我们使用组合键字典保存一些值:

(male, baby, 1)
(male, baby, 2)
(male, baby, 3)
(male, young, 21)
(male, young, 22)
(male, young, 23)
(male, old, 91)
(male, old, 92)
(male, old, 93)
(female, baby, 1)
(female, baby, 2)
(female, baby, 3)
(female, young, 21)
(female, young, 22)
(female, young, 23)
(female, old, 91)
(female, old, 92)
(female, old, 93)
现在,让我们将相同的值保存在字典字典中:

male -> baby ->  1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93
female -> baby ->1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93
在复合键方法中,我保存了“男”和“女”的副本9次,而不是
(male, baby, 1)
(male, baby, 2)
(male, baby, 3)
(male, young, 21)
(male, young, 22)
(male, young, 23)
(male, old, 91)
(male, old, 92)
(male, old, 93)
(female, baby, 1)
(female, baby, 2)
(female, baby, 3)
(female, young, 21)
(female, young, 22)
(female, young, 23)
(female, old, 91)
(female, old, 92)
(female, old, 93)
male -> baby ->  1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93
female -> baby ->1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93
    Dictionary<Tuple<int, int, int>, int> map1 = new Dictionary<Tuple<int, int, int>, int>();
    Dictionary<int, Dictionary<int, Dictionary<int, int>>> map2 = new Dictionary<int, Dictionary<int, Dictionary<int, int>>>();

    public void SizeTest()
    {
        for (int x = 0; x < 30; x++)
        {
            for (int y = 0; y < 100; y++)
            {
                for (int z = 0; z < 600; z++)
                {
                    addToMap1(x, y, z, 0);
                    addToMap2(x, y, z, 0);
                }
            }
        }
        int size1 = GetObjectSize(map1);
        int size2 = GetObjectSize(map2);

        Console.WriteLine(size1);
        Console.WriteLine(size2);
    }

    private void addToMap1(int x, int y, int z, int value)
    {
        map1.Add(new Tuple<int, int, int>(x, y, z), value);
    }

    private void addToMap2(int x, int y, int z, int value)
    {
        map2.GetOrAdd(x, _ => new Dictionary<int, Dictionary<int, int>>())
            .GetOrAdd(y, _ => new Dictionary<int, int>())
            .GetOrAdd(z, _ => value);
    }

    private int GetObjectSize(object TestObject)
    {
        BinaryFormatter bf = new BinaryFormatter();
        MemoryStream ms = new MemoryStream();
        byte[] Array;
        bf.Serialize(ms, TestObject);
        Array = ms.ToArray();
        return Array.Length;
    }

    public static TResult GetOrAdd<TKey, TResult>(this Dictionary<TKey, TResult> map, TKey key, Func<TKey, TResult> addIfMissing)
    {
        TResult result;
        if (!map.TryGetValue(key, out result))
        {
            result = addIfMissing(key);
            map[key] = result;
        }
        return result;
    }