我一直在寻找一种方法来存储和检索超过C#通用Dictionary类提供的单个键的值。
在网上搜索(and on SO itself)向我展示了几个选项:
基于元组的词典
.NET 4.0可以轻松支持通用的Tuple<,>类。这意味着您可以使用任意元组创建一个Dictionary,即
var myDict = new Dictionary<Tuple<Char, Int>, MyClass>();
嵌套词典
我了解到你也可以在Dictionaries中嵌套词典,这使得访问存储的结果类似于访问N维数组。例如:
Dictionary<int, Dictionary<int, Dictionary<Char, MyClass>>>
然后可能被MyClass foo = MyData[8][3]['W'];
分隔的连锁键词典
但是,虽然两个都能很好地处理复杂的数据和自定义类,但我想知道它们是否总是必要的。至少对于原始数据,似乎将键与分隔符连接起来同样有效。
//keys are char + int
Dictionary<string, MyClass> myDict = New Dictionary<string, Myclass>();
String input = myChar + "|" + myInt
MyClass foo = myDict[input]
是否存在使这些方法中的一种优于另一种方案的情况?他们会有类似的表演时间吗?或者重点应该放在哪种方法提供最干净,最容易维护的代码上?
思想?
答案 0 :(得分:14)
分隔的连锁键词典
至少有三个原因可以避免这种方法:
嵌套词典
这解决了分隔符的问题,但引入了一些新问题:
基于元组的词典
在你发布的方法中,这可能是最好的。
但是你可以更进一步,为你的密钥创建一个命名的不可变struct
。这将使您的字典更容易使用,因为密钥的各个部分可以有用。
答案 1 :(得分:3)
或者重点应该放在哪种方法提供最干净,最容易维护的代码上?
除非你专注于编写噩梦般的,令人生畏的代码,否则你应该避免使用字符串分隔和连接方法,而这种做法是不言而喻的。
在元组和嵌套字典之间选择方法取决于您的上下文。调整性能?或者调整可读性?我先谈谈后者。
从可维护性的角度来看,
实现类似的功能要容易得多:
var myDict = new Dictionary<Tuple<char, int>, MyClass>();
大于
var myDict = new Dictionary<char, Dictionary<int, MyClass>>();
来自被叫方。在第二种情况下,每个添加,查找,删除等都需要对多个字典执行操作。
此外,如果您的复合键将来需要一个(或更少)字段,您将需要在第二种情况下更改代码(嵌套字典),因为您必须添加更多嵌套字典和后续检查。
从绩效角度,您可以达到的最佳结论是自己衡量。但是你可以事先考虑一些理论上的限制:
在嵌套字典中,为每个键(外部和内部)添加一个额外的字典会产生一些内存开销(超过创建元组所具有的内容)。
在嵌套字典中,需要在两个字典中执行添加,更新,查找,删除等每个基本操作。现在存在这样的情况:嵌套字典方法可以更快,即,当查找的数据不存在时,因为中间字典可以绕过完整的哈希码计算&amp;比较,但再次确定应该是时间。在数据存在的情况下,它应该更慢,因为查找应该执行两次(或者三次,具体取决于嵌套)。
关于元组方法,.NET元组在它们Equals
and GetHashCode
implementation causes boxing for value types以来被用作集合中的键时,并不是最高效的。
总的来说,我发现很少需要嵌套字典方法。赔率是人们不希望它。我更喜欢基于元组的方法,但是你应该用一个更好的实现编写一个你自己的元组,在这个char
和int
个键的情况下,我更喜欢把它变成一个(不可变的)结构。 / p>
答案 2 :(得分:3)
我想补充一下上面的答案,有一些场景(取决于数据的分布方式),其中嵌套字典在内存占用方面要比复合键字典好得多(这反过来可能导致为了更好的整体表现)。 这样做的原因是嵌套可以节省您为密钥保存重复值的需要,这在大型字典中会使额外字典的占用空间可以忽略不计。
例如,假设我需要一个带有(男/女),(婴儿/年轻/年老),(年龄)复合键的字典。
让我们用复合键词典保存一些值:
(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
在复合键方法中,我保存了一份&#34;男性&#34;和女性&#34; 9次,而不是字典词典中的单个副本。 事实上,我节省了54项与26项,获得了两倍的内存占用。这个例子也有助于可视化差异,看看有多少&#34;空&#34;第二个样本中的空间与第一个样本相比,这些都是我们不需要保存的值。
对于那些仍然不相信的人,这是一个样本测试:
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;
}
此测试返回约30MB vs 70MB,支持字典词典。
答案 3 :(得分:2)
您所描述的所有选项都非常相似 - 至于性能,您需要针对特定的使用场景测试每个选项,但对于小型集合,它们不太可能有太大差异。
他们也都有可读性 - 很难构建它们并从类型中梳理出意义。
相反,最好创建一个直接描述数据的类型 - 良好的命名有很长的路要走。